@wyxos/vibe 1.2.10 → 1.2.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/package.json +1 -1
- package/src/App.vue +15 -3
- package/src/Masonry.vue +21 -8
- package/src/calculateLayout.js +3 -2
- package/src/calculateLayout.test.js +0 -48
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -10,9 +10,21 @@ const masonry = ref(null)
|
|
|
10
10
|
const getPage = async (page) => {
|
|
11
11
|
return new Promise((resolve) => {
|
|
12
12
|
setTimeout(() => {
|
|
13
|
+
// Check if the page exists in the fixture
|
|
14
|
+
const pageData = fixture[page - 1];
|
|
15
|
+
|
|
16
|
+
if (!pageData) {
|
|
17
|
+
// Return empty items if page doesn't exist
|
|
18
|
+
resolve({
|
|
19
|
+
items: [],
|
|
20
|
+
nextPage: null // null indicates no more pages
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
let output = {
|
|
14
|
-
items:
|
|
15
|
-
nextPage: page + 1
|
|
26
|
+
items: pageData.items,
|
|
27
|
+
nextPage: page < fixture.length ? page + 1 : null
|
|
16
28
|
};
|
|
17
29
|
|
|
18
30
|
resolve(output)
|
|
@@ -35,7 +47,7 @@ const getPage = async (page) => {
|
|
|
35
47
|
<p>Loading: <span class="bg-blue-500 text-white p-2 rounded">{{ masonry.isLoading }}</span></p>
|
|
36
48
|
</div>
|
|
37
49
|
</header>
|
|
38
|
-
<masonry v-model:items="items" :get-next-page="getPage" ref="masonry">
|
|
50
|
+
<masonry class="bg-blue-500 " v-model:items="items" :get-next-page="getPage" :load-at-page="1" ref="masonry">
|
|
39
51
|
<template #item="{item, onRemove}">
|
|
40
52
|
<img :src="item.src" class="w-full"/>
|
|
41
53
|
<button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer" @click="onRemove(item)">
|
package/src/Masonry.vue
CHANGED
|
@@ -10,8 +10,8 @@ const props = defineProps({
|
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
loadAtPage: {
|
|
13
|
-
type: Number,
|
|
14
|
-
default:
|
|
13
|
+
type: [Number, String],
|
|
14
|
+
default: null
|
|
15
15
|
},
|
|
16
16
|
items: {
|
|
17
17
|
type: Array,
|
|
@@ -24,6 +24,10 @@ const props = defineProps({
|
|
|
24
24
|
type: String,
|
|
25
25
|
default: 'page', // or 'cursor'
|
|
26
26
|
validator: v => ['page', 'cursor'].includes(v)
|
|
27
|
+
},
|
|
28
|
+
skipInitialLoad: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
default: false
|
|
27
31
|
}
|
|
28
32
|
})
|
|
29
33
|
|
|
@@ -80,6 +84,7 @@ defineExpose({
|
|
|
80
84
|
refreshLayout,
|
|
81
85
|
containerHeight,
|
|
82
86
|
onRemove,
|
|
87
|
+
loadNext
|
|
83
88
|
})
|
|
84
89
|
|
|
85
90
|
async function onScroll() {
|
|
@@ -237,11 +242,19 @@ onMounted(async () => {
|
|
|
237
242
|
|
|
238
243
|
columns.value = getColumnCount()
|
|
239
244
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
// For cursor-based pagination, loadAtPage can be null for the first request
|
|
246
|
+
const initialPage = props.loadAtPage
|
|
247
|
+
paginationHistory.value = [initialPage]
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
// Skip initial load if skipInitialLoad prop is true
|
|
250
|
+
if (!props.skipInitialLoad) {
|
|
251
|
+
const response = await getContent(paginationHistory.value[0])
|
|
252
|
+
paginationHistory.value.push(response.nextPage)
|
|
253
|
+
} else {
|
|
254
|
+
await nextTick()
|
|
255
|
+
// Just refresh the layout with any existing items
|
|
256
|
+
refreshLayout(masonry.value)
|
|
257
|
+
}
|
|
245
258
|
|
|
246
259
|
isLoading.value = false
|
|
247
260
|
|
|
@@ -258,13 +271,13 @@ onUnmounted(() => {
|
|
|
258
271
|
</script>
|
|
259
272
|
|
|
260
273
|
<template>
|
|
261
|
-
<div class="overflow-auto
|
|
274
|
+
<div class="overflow-auto w-full flex-1" ref="container">
|
|
262
275
|
<div class="relative" :style="{height: `${containerHeight}px`}">
|
|
263
276
|
<transition-group :css="false" @enter="onEnter" @before-enter="onBeforeEnter"
|
|
264
277
|
@leave="onLeave"
|
|
265
278
|
@before-leave="onBeforeLeave">
|
|
266
279
|
<div v-for="item in masonry" :key="`${item.page}-${item.id}`"
|
|
267
|
-
class="
|
|
280
|
+
class="absolute transition-[top,left,opacity] duration-500 ease-in-out"
|
|
268
281
|
v-bind="itemAttributes(item)">
|
|
269
282
|
<slot name="item" v-bind="{item, onRemove}">
|
|
270
283
|
<img :src="item.src" class="w-full"/>
|
package/src/calculateLayout.js
CHANGED
|
@@ -60,8 +60,9 @@ export default function calculateLayout(items, container, columnCount, options =
|
|
|
60
60
|
|
|
61
61
|
newItem.columnWidth = columnWidth;
|
|
62
62
|
newItem.left = col * (columnWidth + gutterX);
|
|
63
|
-
newItem.
|
|
64
|
-
newItem.
|
|
63
|
+
newItem.imageHeight = Math.round((columnWidth * originalHeight) / originalWidth);
|
|
64
|
+
newItem.columnHeight = newItem.imageHeight + footer + header;
|
|
65
|
+
newItem.top = columnHeights[col];
|
|
65
66
|
|
|
66
67
|
columnHeights[col] += newItem.columnHeight + gutterY;
|
|
67
68
|
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import calculateLayout from './calculateLayout'
|
|
3
|
-
import fixture from './pages.json'
|
|
4
|
-
|
|
5
|
-
describe('calculateLayout', () => {
|
|
6
|
-
it('should calculate correct layout positions for known inputs', () => {
|
|
7
|
-
const items = fixture[0].items.slice(0, 3) // first 3 items
|
|
8
|
-
const mockContainer = {
|
|
9
|
-
offsetWidth: 1000,
|
|
10
|
-
clientWidth: 980 // scrollbarWidth = 20
|
|
11
|
-
}
|
|
12
|
-
const columnCount = 3
|
|
13
|
-
const gutterX = 10
|
|
14
|
-
const gutterY = 10
|
|
15
|
-
|
|
16
|
-
const scrollbarWidth = mockContainer.offsetWidth - mockContainer.clientWidth
|
|
17
|
-
const usableWidth = mockContainer.offsetWidth - scrollbarWidth
|
|
18
|
-
const totalGutterX = gutterX * (columnCount - 1)
|
|
19
|
-
const expectedColumnWidth = Math.floor((usableWidth - totalGutterX) / columnCount)
|
|
20
|
-
|
|
21
|
-
const result = calculateLayout(items, mockContainer, columnCount, gutterX, gutterY)
|
|
22
|
-
|
|
23
|
-
expect(result.length).toBe(3)
|
|
24
|
-
|
|
25
|
-
result.forEach((item, i) => {
|
|
26
|
-
const original = items[i]
|
|
27
|
-
const expectedColumn = i % columnCount
|
|
28
|
-
const expectedLeft = expectedColumn * (expectedColumnWidth + gutterX)
|
|
29
|
-
const expectedHeight = Math.round((expectedColumnWidth * original.height) / original.width)
|
|
30
|
-
|
|
31
|
-
expect(item.left).toBe(expectedLeft)
|
|
32
|
-
expect(item.columnWidth).toBe(expectedColumnWidth)
|
|
33
|
-
expect(item.columnHeight).toBe(expectedHeight)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// Test top stacking logic: second item in same column should be placed below with gutter
|
|
37
|
-
const secondBatch = fixture[0].items.slice(0, 6)
|
|
38
|
-
const stacked = calculateLayout(secondBatch, mockContainer, columnCount, gutterX, gutterY)
|
|
39
|
-
for (let col = 0; col < columnCount; col++) {
|
|
40
|
-
const colItems = stacked.filter((_, i) => i % columnCount === col)
|
|
41
|
-
for (let j = 1; j < colItems.length; j++) {
|
|
42
|
-
expect(colItems[j].top).toBe(
|
|
43
|
-
colItems[j - 1].top + colItems[j - 1].columnHeight + gutterY
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
})
|