@wyxos/vibe 1.2.15 → 1.3.0
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/package.json +1 -1
- package/src/Masonry.vue +124 -30
- package/src/calculateLayout.js +2 -1
- package/src/useMasonryScroll.js +56 -26
package/package.json
CHANGED
package/src/Masonry.vue
CHANGED
|
@@ -40,6 +40,10 @@ const props = defineProps({
|
|
|
40
40
|
maxItems: {
|
|
41
41
|
type: Number,
|
|
42
42
|
default: 100
|
|
43
|
+
},
|
|
44
|
+
pageSize: {
|
|
45
|
+
type: Number,
|
|
46
|
+
default: 40
|
|
43
47
|
}
|
|
44
48
|
})
|
|
45
49
|
|
|
@@ -81,10 +85,31 @@ const isLoading = ref(false)
|
|
|
81
85
|
|
|
82
86
|
const containerHeight = ref(0)
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
// Scroll progress tracking
|
|
89
|
+
const scrollProgress = ref({
|
|
90
|
+
distanceToTrigger: 0,
|
|
91
|
+
isNearTrigger: false
|
|
86
92
|
})
|
|
87
93
|
|
|
94
|
+
const updateScrollProgress = () => {
|
|
95
|
+
if (!container.value) return
|
|
96
|
+
|
|
97
|
+
const { scrollTop, clientHeight } = container.value
|
|
98
|
+
const visibleBottom = scrollTop + clientHeight
|
|
99
|
+
|
|
100
|
+
const columnHeights = calculateColumnHeights(masonry.value, columns.value)
|
|
101
|
+
const shortestColumn = Math.min(...columnHeights)
|
|
102
|
+
const triggerPoint = shortestColumn - 300 // Same threshold as in scroll handler
|
|
103
|
+
|
|
104
|
+
const distanceToTrigger = Math.max(0, triggerPoint - visibleBottom)
|
|
105
|
+
const isNearTrigger = distanceToTrigger <= 100
|
|
106
|
+
|
|
107
|
+
scrollProgress.value = {
|
|
108
|
+
distanceToTrigger: Math.round(distanceToTrigger),
|
|
109
|
+
isNearTrigger
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
// Setup composables
|
|
89
114
|
const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } = useMasonryTransitions(masonry)
|
|
90
115
|
|
|
@@ -95,6 +120,7 @@ const { handleScroll } = useMasonryScroll({
|
|
|
95
120
|
containerHeight,
|
|
96
121
|
isLoading,
|
|
97
122
|
maxItems: props.maxItems,
|
|
123
|
+
pageSize: props.pageSize,
|
|
98
124
|
refreshLayout,
|
|
99
125
|
loadNext
|
|
100
126
|
})
|
|
@@ -105,7 +131,8 @@ defineExpose({
|
|
|
105
131
|
containerHeight,
|
|
106
132
|
onRemove,
|
|
107
133
|
loadNext,
|
|
108
|
-
loadPage
|
|
134
|
+
loadPage,
|
|
135
|
+
reset
|
|
109
136
|
})
|
|
110
137
|
|
|
111
138
|
function calculateHeight(content) {
|
|
@@ -129,14 +156,38 @@ async function getContent(page) {
|
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
async function loadPage(page) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
if (isLoading.value) return // Prevent concurrent loading
|
|
160
|
+
|
|
161
|
+
isLoading.value = true
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const response = await getContent(page)
|
|
165
|
+
paginationHistory.value.push(response.nextPage)
|
|
166
|
+
return response
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Error loading page:', error)
|
|
169
|
+
throw error
|
|
170
|
+
} finally {
|
|
171
|
+
isLoading.value = false
|
|
172
|
+
}
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
async function loadNext() {
|
|
138
|
-
|
|
139
|
-
|
|
176
|
+
if (isLoading.value) return // Prevent concurrent loading
|
|
177
|
+
|
|
178
|
+
isLoading.value = true
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const currentPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
182
|
+
const response = await getContent(currentPage)
|
|
183
|
+
paginationHistory.value.push(response.nextPage)
|
|
184
|
+
return response
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('Error loading next page:', error)
|
|
187
|
+
throw error
|
|
188
|
+
} finally {
|
|
189
|
+
isLoading.value = false
|
|
190
|
+
}
|
|
140
191
|
}
|
|
141
192
|
|
|
142
193
|
function onRemove(item) {
|
|
@@ -148,35 +199,69 @@ function onResize() {
|
|
|
148
199
|
refreshLayout(masonry.value)
|
|
149
200
|
}
|
|
150
201
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
202
|
+
function reset() {
|
|
203
|
+
// Scroll back to top first (while items still exist to scroll through)
|
|
204
|
+
if (container.value) {
|
|
205
|
+
container.value.scrollTo({
|
|
206
|
+
top: 0,
|
|
207
|
+
behavior: 'smooth'
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Clear all items
|
|
212
|
+
masonry.value = []
|
|
213
|
+
|
|
214
|
+
// Reset container height
|
|
215
|
+
containerHeight.value = 0
|
|
216
|
+
|
|
217
|
+
// Reset pagination history to initial state
|
|
218
|
+
paginationHistory.value = [props.loadAtPage]
|
|
219
|
+
|
|
220
|
+
// Reset scroll progress
|
|
221
|
+
scrollProgress.value = {
|
|
222
|
+
distanceToTrigger: 0,
|
|
223
|
+
isNearTrigger: false
|
|
167
224
|
}
|
|
225
|
+
}
|
|
168
226
|
|
|
169
|
-
|
|
227
|
+
// Create debounced functions with stable references
|
|
228
|
+
const debouncedScrollHandler = debounce(() => {
|
|
229
|
+
handleScroll()
|
|
230
|
+
updateScrollProgress()
|
|
231
|
+
}, 200)
|
|
170
232
|
|
|
171
|
-
|
|
233
|
+
const debouncedResizeHandler = debounce(onResize, 200)
|
|
172
234
|
|
|
173
|
-
|
|
235
|
+
onMounted(async () => {
|
|
236
|
+
try {
|
|
237
|
+
columns.value = getColumnCount(layout.value)
|
|
238
|
+
|
|
239
|
+
// For cursor-based pagination, loadAtPage can be null for the first request
|
|
240
|
+
const initialPage = props.loadAtPage
|
|
241
|
+
paginationHistory.value = [initialPage]
|
|
242
|
+
|
|
243
|
+
// Skip initial load if skipInitialLoad prop is true
|
|
244
|
+
if (!props.skipInitialLoad) {
|
|
245
|
+
await loadPage(paginationHistory.value[0]) // loadPage manages its own loading state
|
|
246
|
+
} else {
|
|
247
|
+
await nextTick()
|
|
248
|
+
// Just refresh the layout with any existing items
|
|
249
|
+
refreshLayout(masonry.value)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
updateScrollProgress()
|
|
253
|
+
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error('Error during component initialization:', error)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
container.value?.addEventListener('scroll', debouncedScrollHandler)
|
|
259
|
+
window.addEventListener('resize', debouncedResizeHandler)
|
|
174
260
|
})
|
|
175
261
|
|
|
176
262
|
onUnmounted(() => {
|
|
177
|
-
container.value?.removeEventListener('scroll',
|
|
178
|
-
|
|
179
|
-
window.removeEventListener('resize', debounce(onResize, 200));
|
|
263
|
+
container.value?.removeEventListener('scroll', debouncedScrollHandler)
|
|
264
|
+
window.removeEventListener('resize', debouncedResizeHandler)
|
|
180
265
|
})
|
|
181
266
|
</script>
|
|
182
267
|
|
|
@@ -198,6 +283,15 @@ onUnmounted(() => {
|
|
|
198
283
|
</slot>
|
|
199
284
|
</div>
|
|
200
285
|
</transition-group>
|
|
286
|
+
|
|
287
|
+
<!-- Scroll Progress Badge -->
|
|
288
|
+
<div v-if="containerHeight > 0"
|
|
289
|
+
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"
|
|
290
|
+
:class="{'opacity-50 hover:opacity-100': !scrollProgress.isNearTrigger, 'opacity-100': scrollProgress.isNearTrigger}">
|
|
291
|
+
<span>{{ masonry.length }} items</span>
|
|
292
|
+
<span class="mx-2">|</span>
|
|
293
|
+
<span>{{ scrollProgress.distanceToTrigger }}px to load</span>
|
|
294
|
+
</div>
|
|
201
295
|
</div>
|
|
202
296
|
</div>
|
|
203
297
|
</template>
|
package/src/calculateLayout.js
CHANGED
|
@@ -54,7 +54,8 @@ export default function calculateLayout(items, container, columnCount, options =
|
|
|
54
54
|
const item = items[index];
|
|
55
55
|
const newItem = { ...item };
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// Find the column with the shortest height for proper masonry layout
|
|
58
|
+
const col = columnHeights.indexOf(Math.min(...columnHeights));
|
|
58
59
|
const originalWidth = item.width;
|
|
59
60
|
const originalHeight = item.height;
|
|
60
61
|
|
package/src/useMasonryScroll.js
CHANGED
|
@@ -11,10 +11,10 @@ export function useMasonryScroll({
|
|
|
11
11
|
containerHeight,
|
|
12
12
|
isLoading,
|
|
13
13
|
maxItems,
|
|
14
|
+
pageSize,
|
|
14
15
|
refreshLayout,
|
|
15
16
|
loadNext
|
|
16
17
|
}) {
|
|
17
|
-
|
|
18
18
|
async function handleScroll() {
|
|
19
19
|
const { scrollTop, clientHeight } = container.value
|
|
20
20
|
const visibleBottom = scrollTop + clientHeight
|
|
@@ -24,56 +24,86 @@ export function useMasonryScroll({
|
|
|
24
24
|
const reachedContainerBottom = scrollTop + clientHeight >= containerHeight.value - 1
|
|
25
25
|
|
|
26
26
|
if ((whitespaceVisible || reachedContainerBottom) && !isLoading.value) {
|
|
27
|
-
|
|
27
|
+
try {
|
|
28
|
+
// Handle cleanup when too many items
|
|
29
|
+
if (masonry.value.length > maxItems) {
|
|
30
|
+
await handleItemCleanup(columnHeights)
|
|
31
|
+
}
|
|
28
32
|
|
|
29
|
-
// Handle cleanup when too many items
|
|
30
|
-
if (masonry.value.length > maxItems) {
|
|
31
|
-
await handleItemCleanup(columnHeights)
|
|
32
|
-
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
await loadNext() // loadNext manages its own loading state
|
|
35
|
+
await nextTick()
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error in scroll handler:', error)
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
async function handleItemCleanup(columnHeights) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!firstItem) {
|
|
44
|
-
await loadNext()
|
|
45
|
-
await nextTick()
|
|
46
|
-
isLoading.value = false
|
|
43
|
+
if (!masonry.value.length) {
|
|
47
44
|
return
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
if(masonry.value.length <= pageSize) {
|
|
48
|
+
// If we have fewer items than pageSize, no cleanup needed
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Group items by page to understand page structure
|
|
53
|
+
const pageGroups = masonry.value.reduce((acc, item) => {
|
|
54
|
+
if (!acc[item.page]) {
|
|
55
|
+
acc[item.page] = []
|
|
56
|
+
}
|
|
57
|
+
acc[item.page].push(item)
|
|
58
|
+
return acc
|
|
59
|
+
}, {})
|
|
60
|
+
|
|
61
|
+
const pages = Object.keys(pageGroups).sort((a, b) => parseInt(a) - parseInt(b))
|
|
62
|
+
|
|
63
|
+
if (pages.length === 0) {
|
|
57
64
|
return
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
let totalRemovedItems = 0
|
|
68
|
+
let pagesToRemove = []
|
|
69
|
+
|
|
70
|
+
// Remove pages cumulatively until we reach at least pageSize items
|
|
71
|
+
for (const page of pages) {
|
|
72
|
+
pagesToRemove.push(page)
|
|
73
|
+
totalRemovedItems += pageGroups[page].length
|
|
74
|
+
|
|
75
|
+
if (totalRemovedItems >= pageSize) {
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Filter out items from pages to be removed
|
|
81
|
+
const remainingItems = masonry.value.filter(item => !pagesToRemove.includes(item.page.toString()))
|
|
82
|
+
|
|
83
|
+
if (remainingItems.length === masonry.value.length) {
|
|
84
|
+
// No items were removed, nothing to do
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
refreshLayout(remainingItems)
|
|
62
89
|
|
|
90
|
+
|
|
91
|
+
await nextTick()
|
|
92
|
+
|
|
63
93
|
await adjustScrollPosition(columnHeights)
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
async function adjustScrollPosition(columnHeights) {
|
|
67
97
|
const lowestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights))
|
|
68
98
|
const lastItemInColumn = masonry.value.filter((_, index) => index % columns.value === lowestColumnIndex).pop()
|
|
69
|
-
|
|
99
|
+
|
|
70
100
|
if (lastItemInColumn) {
|
|
71
101
|
const lastItemInColumnTop = lastItemInColumn.top + lastItemInColumn.columnHeight
|
|
72
102
|
const lastItemInColumnBottom = lastItemInColumnTop + lastItemInColumn.columnHeight
|
|
73
103
|
const containerTop = container.value.scrollTop
|
|
74
104
|
const containerBottom = containerTop + container.value.clientHeight
|
|
75
105
|
const itemInView = lastItemInColumnTop >= containerTop && lastItemInColumnBottom <= containerBottom
|
|
76
|
-
|
|
106
|
+
|
|
77
107
|
if (!itemInView) {
|
|
78
108
|
container.value.scrollTo({
|
|
79
109
|
top: lastItemInColumnTop - 10,
|