@wyxos/vibe 1.6.21 → 1.6.22
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 +906 -829
- package/lib/vibe.css +1 -1
- package/package.json +1 -1
- package/src/Masonry.vue +1646 -1622
- package/src/components/MasonryItem.vue +4 -1
- package/src/useMasonryScroll.ts +60 -60
- package/src/useSwipeMode.ts +233 -0
- package/src/utils/errorHandler.ts +8 -0
|
@@ -392,7 +392,10 @@ watch(
|
|
|
392
392
|
>
|
|
393
393
|
<!-- Media type indicator badge - shown BEFORE preloading starts -->
|
|
394
394
|
<div class="w-12 h-12 rounded-full bg-white/80 backdrop-blur-sm flex items-center justify-center shadow-sm">
|
|
395
|
-
|
|
395
|
+
<!-- Allow custom icon via slot, fallback to default -->
|
|
396
|
+
<slot name="placeholder-icon" :mediaType="mediaType">
|
|
397
|
+
<i :class="mediaType === 'video' ? 'fas fa-video text-xl text-slate-400' : 'fas fa-image text-xl text-slate-400'"></i>
|
|
398
|
+
</slot>
|
|
396
399
|
</div>
|
|
397
400
|
</div>
|
|
398
401
|
|
package/src/useMasonryScroll.ts
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { nextTick, type Ref } from 'vue'
|
|
2
|
-
import { calculateColumnHeights } from './masonryUtils'
|
|
3
|
-
import type { ProcessedMasonryItem } from './types'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Composable for handling masonry scroll behavior and item cleanup
|
|
7
|
-
*/
|
|
8
|
-
export function useMasonryScroll({
|
|
9
|
-
container,
|
|
10
|
-
masonry,
|
|
11
|
-
columns,
|
|
12
|
-
containerHeight,
|
|
13
|
-
isLoading,
|
|
14
|
-
pageSize,
|
|
15
|
-
refreshLayout,
|
|
16
|
-
setItemsRaw,
|
|
17
|
-
loadNext,
|
|
18
|
-
loadThresholdPx
|
|
19
|
-
}: {
|
|
20
|
-
container: Ref<HTMLElement | null>
|
|
21
|
-
masonry: Ref<ProcessedMasonryItem[]>
|
|
22
|
-
columns: Ref<number>
|
|
23
|
-
containerHeight: Ref<number>
|
|
24
|
-
isLoading: Ref<boolean>
|
|
25
|
-
pageSize: number
|
|
26
|
-
refreshLayout: (items: ProcessedMasonryItem[]) => void
|
|
27
|
-
setItemsRaw: (items: ProcessedMasonryItem[]) => void
|
|
28
|
-
loadNext: () => Promise<any>
|
|
29
|
-
loadThresholdPx?: number
|
|
30
|
-
}) {
|
|
31
|
-
let cleanupInProgress = false
|
|
32
|
-
let lastScrollTop = 0
|
|
33
|
-
|
|
34
|
-
async function handleScroll(precomputedHeights?: number[]) {
|
|
35
|
-
if (!container.value) return
|
|
36
|
-
|
|
37
|
-
const columnHeights = precomputedHeights ?? calculateColumnHeights(masonry.value, columns.value)
|
|
38
|
-
const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
|
|
39
|
-
const scrollerBottom = container.value.scrollTop + container.value.clientHeight
|
|
40
|
-
|
|
41
|
-
const isScrollingDown = container.value.scrollTop > lastScrollTop + 1 // tolerate tiny jitter
|
|
42
|
-
lastScrollTop = container.value.scrollTop
|
|
43
|
-
|
|
44
|
-
const threshold = typeof loadThresholdPx === 'number' ? loadThresholdPx : 200
|
|
45
|
-
const triggerPoint = threshold >= 0
|
|
46
|
-
? Math.max(0, tallest - threshold)
|
|
47
|
-
: Math.max(0, tallest + threshold)
|
|
48
|
-
const nearBottom = scrollerBottom >= triggerPoint
|
|
49
|
-
|
|
50
|
-
if (nearBottom && isScrollingDown && !isLoading.value) {
|
|
51
|
-
await loadNext()
|
|
52
|
-
await nextTick()
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
handleScroll
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
import { nextTick, type Ref } from 'vue'
|
|
2
|
+
import { calculateColumnHeights } from './masonryUtils'
|
|
3
|
+
import type { ProcessedMasonryItem } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Composable for handling masonry scroll behavior and item cleanup
|
|
7
|
+
*/
|
|
8
|
+
export function useMasonryScroll({
|
|
9
|
+
container,
|
|
10
|
+
masonry,
|
|
11
|
+
columns,
|
|
12
|
+
containerHeight,
|
|
13
|
+
isLoading,
|
|
14
|
+
pageSize,
|
|
15
|
+
refreshLayout,
|
|
16
|
+
setItemsRaw,
|
|
17
|
+
loadNext,
|
|
18
|
+
loadThresholdPx
|
|
19
|
+
}: {
|
|
20
|
+
container: Ref<HTMLElement | null>
|
|
21
|
+
masonry: Ref<ProcessedMasonryItem[]>
|
|
22
|
+
columns: Ref<number>
|
|
23
|
+
containerHeight: Ref<number>
|
|
24
|
+
isLoading: Ref<boolean>
|
|
25
|
+
pageSize: number
|
|
26
|
+
refreshLayout: (items: ProcessedMasonryItem[]) => void
|
|
27
|
+
setItemsRaw: (items: ProcessedMasonryItem[]) => void
|
|
28
|
+
loadNext: () => Promise<any>
|
|
29
|
+
loadThresholdPx?: number
|
|
30
|
+
}) {
|
|
31
|
+
let cleanupInProgress = false
|
|
32
|
+
let lastScrollTop = 0
|
|
33
|
+
|
|
34
|
+
async function handleScroll(precomputedHeights?: number[]) {
|
|
35
|
+
if (!container.value) return
|
|
36
|
+
|
|
37
|
+
const columnHeights = precomputedHeights ?? calculateColumnHeights(masonry.value, columns.value)
|
|
38
|
+
const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
|
|
39
|
+
const scrollerBottom = container.value.scrollTop + container.value.clientHeight
|
|
40
|
+
|
|
41
|
+
const isScrollingDown = container.value.scrollTop > lastScrollTop + 1 // tolerate tiny jitter
|
|
42
|
+
lastScrollTop = container.value.scrollTop
|
|
43
|
+
|
|
44
|
+
const threshold = typeof loadThresholdPx === 'number' ? loadThresholdPx : 200
|
|
45
|
+
const triggerPoint = threshold >= 0
|
|
46
|
+
? Math.max(0, tallest - threshold)
|
|
47
|
+
: Math.max(0, tallest + threshold)
|
|
48
|
+
const nearBottom = scrollerBottom >= triggerPoint
|
|
49
|
+
|
|
50
|
+
if (nearBottom && isScrollingDown && !isLoading.value) {
|
|
51
|
+
await loadNext()
|
|
52
|
+
await nextTick()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
handleScroll
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { computed, ref, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface UseSwipeModeOptions {
|
|
4
|
+
useSwipeMode: ComputedRef<boolean>
|
|
5
|
+
masonry: Ref<any[]>
|
|
6
|
+
isLoading: Ref<boolean>
|
|
7
|
+
loadNext: () => Promise<any>
|
|
8
|
+
loadPage: (page: any) => Promise<any>
|
|
9
|
+
paginationHistory: Ref<any[]>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseSwipeModeReturn {
|
|
13
|
+
// State
|
|
14
|
+
currentSwipeIndex: Ref<number>
|
|
15
|
+
swipeOffset: Ref<number>
|
|
16
|
+
isDragging: Ref<boolean>
|
|
17
|
+
swipeContainer: Ref<HTMLElement | null>
|
|
18
|
+
|
|
19
|
+
// Computed
|
|
20
|
+
currentItem: ComputedRef<any | null>
|
|
21
|
+
nextItem: ComputedRef<any | null>
|
|
22
|
+
previousItem: ComputedRef<any | null>
|
|
23
|
+
|
|
24
|
+
// Functions
|
|
25
|
+
handleTouchStart: (e: TouchEvent) => void
|
|
26
|
+
handleTouchMove: (e: TouchEvent) => void
|
|
27
|
+
handleTouchEnd: (e: TouchEvent) => void
|
|
28
|
+
handleMouseDown: (e: MouseEvent) => void
|
|
29
|
+
handleMouseMove: (e: MouseEvent) => void
|
|
30
|
+
handleMouseUp: (e: MouseEvent) => void
|
|
31
|
+
goToNextItem: () => void
|
|
32
|
+
goToPreviousItem: () => void
|
|
33
|
+
snapToCurrentItem: () => void
|
|
34
|
+
handleWindowResize: () => void
|
|
35
|
+
reset: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useSwipeMode(options: UseSwipeModeOptions): UseSwipeModeReturn {
|
|
39
|
+
const { useSwipeMode, masonry, isLoading, loadNext, loadPage, paginationHistory } = options
|
|
40
|
+
|
|
41
|
+
// Swipe mode state
|
|
42
|
+
const currentSwipeIndex = ref<number>(0)
|
|
43
|
+
const swipeOffset = ref<number>(0)
|
|
44
|
+
const isDragging = ref<boolean>(false)
|
|
45
|
+
const dragStartY = ref<number>(0)
|
|
46
|
+
const dragStartOffset = ref<number>(0)
|
|
47
|
+
const swipeContainer = ref<HTMLElement | null>(null)
|
|
48
|
+
|
|
49
|
+
// Get current item index for swipe mode
|
|
50
|
+
const currentItem = computed(() => {
|
|
51
|
+
if (!useSwipeMode.value || masonry.value.length === 0) return null
|
|
52
|
+
const index = Math.max(0, Math.min(currentSwipeIndex.value, masonry.value.length - 1))
|
|
53
|
+
return masonry.value[index] || null
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Get next/previous items for preloading in swipe mode
|
|
57
|
+
const nextItem = computed(() => {
|
|
58
|
+
if (!useSwipeMode.value || !currentItem.value) return null
|
|
59
|
+
const nextIndex = currentSwipeIndex.value + 1
|
|
60
|
+
if (nextIndex >= masonry.value.length) return null
|
|
61
|
+
return masonry.value[nextIndex] || null
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const previousItem = computed(() => {
|
|
65
|
+
if (!useSwipeMode.value || !currentItem.value) return null
|
|
66
|
+
const prevIndex = currentSwipeIndex.value - 1
|
|
67
|
+
if (prevIndex < 0) return null
|
|
68
|
+
return masonry.value[prevIndex] || null
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function snapToCurrentItem() {
|
|
72
|
+
if (!swipeContainer.value) return
|
|
73
|
+
|
|
74
|
+
// Use container height for swipe mode instead of window height
|
|
75
|
+
const viewportHeight = swipeContainer.value.clientHeight
|
|
76
|
+
swipeOffset.value = -currentSwipeIndex.value * viewportHeight
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function goToNextItem() {
|
|
80
|
+
if (!nextItem.value) {
|
|
81
|
+
// Try to load next page
|
|
82
|
+
loadNext()
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
currentSwipeIndex.value++
|
|
87
|
+
snapToCurrentItem()
|
|
88
|
+
|
|
89
|
+
// Preload next item if we're near the end
|
|
90
|
+
if (currentSwipeIndex.value >= masonry.value.length - 5) {
|
|
91
|
+
loadNext()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function goToPreviousItem() {
|
|
96
|
+
if (!previousItem.value) return
|
|
97
|
+
|
|
98
|
+
currentSwipeIndex.value--
|
|
99
|
+
snapToCurrentItem()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Swipe gesture handlers
|
|
103
|
+
function handleTouchStart(e: TouchEvent) {
|
|
104
|
+
if (!useSwipeMode.value) return
|
|
105
|
+
isDragging.value = true
|
|
106
|
+
dragStartY.value = e.touches[0].clientY
|
|
107
|
+
dragStartOffset.value = swipeOffset.value
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleTouchMove(e: TouchEvent) {
|
|
112
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
113
|
+
const deltaY = e.touches[0].clientY - dragStartY.value
|
|
114
|
+
swipeOffset.value = dragStartOffset.value + deltaY
|
|
115
|
+
e.preventDefault()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleTouchEnd(e: TouchEvent) {
|
|
119
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
120
|
+
isDragging.value = false
|
|
121
|
+
|
|
122
|
+
const deltaY = swipeOffset.value - dragStartOffset.value
|
|
123
|
+
const threshold = 100 // Minimum swipe distance to trigger navigation
|
|
124
|
+
|
|
125
|
+
if (Math.abs(deltaY) > threshold) {
|
|
126
|
+
if (deltaY > 0 && previousItem.value) {
|
|
127
|
+
// Swipe down - go to previous
|
|
128
|
+
goToPreviousItem()
|
|
129
|
+
} else if (deltaY < 0 && nextItem.value) {
|
|
130
|
+
// Swipe up - go to next
|
|
131
|
+
goToNextItem()
|
|
132
|
+
} else {
|
|
133
|
+
// Snap back
|
|
134
|
+
snapToCurrentItem()
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Snap back if swipe wasn't far enough
|
|
138
|
+
snapToCurrentItem()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
e.preventDefault()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Mouse drag handlers for desktop testing
|
|
145
|
+
function handleMouseDown(e: MouseEvent) {
|
|
146
|
+
if (!useSwipeMode.value) return
|
|
147
|
+
isDragging.value = true
|
|
148
|
+
dragStartY.value = e.clientY
|
|
149
|
+
dragStartOffset.value = swipeOffset.value
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleMouseMove(e: MouseEvent) {
|
|
154
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
155
|
+
const deltaY = e.clientY - dragStartY.value
|
|
156
|
+
swipeOffset.value = dragStartOffset.value + deltaY
|
|
157
|
+
e.preventDefault()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handleMouseUp(e: MouseEvent) {
|
|
161
|
+
if (!useSwipeMode.value || !isDragging.value) return
|
|
162
|
+
isDragging.value = false
|
|
163
|
+
|
|
164
|
+
const deltaY = swipeOffset.value - dragStartOffset.value
|
|
165
|
+
const threshold = 100
|
|
166
|
+
|
|
167
|
+
if (Math.abs(deltaY) > threshold) {
|
|
168
|
+
if (deltaY > 0 && previousItem.value) {
|
|
169
|
+
goToPreviousItem()
|
|
170
|
+
} else if (deltaY < 0 && nextItem.value) {
|
|
171
|
+
goToNextItem()
|
|
172
|
+
} else {
|
|
173
|
+
snapToCurrentItem()
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
snapToCurrentItem()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
e.preventDefault()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Watch for container/window resize to update swipe mode
|
|
183
|
+
function handleWindowResize() {
|
|
184
|
+
// If switching from swipe to masonry, reset swipe state
|
|
185
|
+
if (!useSwipeMode.value && currentSwipeIndex.value > 0) {
|
|
186
|
+
currentSwipeIndex.value = 0
|
|
187
|
+
swipeOffset.value = 0
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// If switching to swipe mode, ensure we have items loaded
|
|
191
|
+
if (useSwipeMode.value && masonry.value.length === 0 && !isLoading.value) {
|
|
192
|
+
loadPage(paginationHistory.value[0] as any)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Re-snap to current item on resize to adjust offset
|
|
196
|
+
if (useSwipeMode.value) {
|
|
197
|
+
snapToCurrentItem()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function reset() {
|
|
202
|
+
currentSwipeIndex.value = 0
|
|
203
|
+
swipeOffset.value = 0
|
|
204
|
+
isDragging.value = false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
// State
|
|
209
|
+
currentSwipeIndex,
|
|
210
|
+
swipeOffset,
|
|
211
|
+
isDragging,
|
|
212
|
+
swipeContainer,
|
|
213
|
+
|
|
214
|
+
// Computed
|
|
215
|
+
currentItem,
|
|
216
|
+
nextItem,
|
|
217
|
+
previousItem,
|
|
218
|
+
|
|
219
|
+
// Functions
|
|
220
|
+
handleTouchStart,
|
|
221
|
+
handleTouchMove,
|
|
222
|
+
handleTouchEnd,
|
|
223
|
+
handleMouseDown,
|
|
224
|
+
handleMouseMove,
|
|
225
|
+
handleMouseUp,
|
|
226
|
+
goToNextItem,
|
|
227
|
+
goToPreviousItem,
|
|
228
|
+
snapToCurrentItem,
|
|
229
|
+
handleWindowResize,
|
|
230
|
+
reset
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|