@wyxos/vibe 1.2.11 → 1.2.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/vibe",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "main": "index.js",
5
5
  "module": "index.js",
6
6
  "type": "module",
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: fixture[page - 1].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 class="bg-blue-500 " 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: 1
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() {
@@ -94,14 +99,31 @@ async function onScroll() {
94
99
  isLoading.value = true
95
100
 
96
101
  if (paginationHistory.value.length > 3) {
97
- // get first item
102
+ // get first item - only proceed if it exists
98
103
  const firstItem = masonry.value[0]
104
+
105
+ if (!firstItem) {
106
+ // Skip removal logic if there are no items
107
+ await loadNext()
108
+ await nextTick()
109
+ isLoading.value = false
110
+ return
111
+ }
99
112
 
100
113
  // get page number
101
114
  const page = firstItem.page
102
115
 
103
116
  // find all item with this page
104
117
  const removedItems = masonry.value.filter(i => i.page !== page)
118
+
119
+ // Only proceed with removal if there are actually items to remove
120
+ if (removedItems.length === masonry.value.length) {
121
+ // All items belong to the same page, skip removal logic
122
+ await loadNext()
123
+ await nextTick()
124
+ isLoading.value = false
125
+ return
126
+ }
105
127
 
106
128
  refreshLayout(removedItems)
107
129
 
@@ -111,16 +133,20 @@ async function onScroll() {
111
133
 
112
134
  // find the last item in that column
113
135
  const lastItemInColumn = masonry.value.filter((_, index) => index % columns.value === lowestColumnIndex).pop()
114
- const lastItemInColumnTop = lastItemInColumn.top + lastItemInColumn.columnHeight
115
- const lastItemInColumnBottom = lastItemInColumnTop + lastItemInColumn.columnHeight
116
- const containerTop = container.value.scrollTop
117
- const containerBottom = containerTop + container.value.clientHeight
118
- const itemInView = lastItemInColumnTop >= containerTop && lastItemInColumnBottom <= containerBottom
119
- if (!itemInView) {
120
- container.value.scrollTo({
121
- top: lastItemInColumnTop - 10,
122
- behavior: 'smooth'
123
- })
136
+
137
+ // Only proceed with scroll adjustment if we have a valid item
138
+ if (lastItemInColumn) {
139
+ const lastItemInColumnTop = lastItemInColumn.top + lastItemInColumn.columnHeight
140
+ const lastItemInColumnBottom = lastItemInColumnTop + lastItemInColumn.columnHeight
141
+ const containerTop = container.value.scrollTop
142
+ const containerBottom = containerTop + container.value.clientHeight
143
+ const itemInView = lastItemInColumnTop >= containerTop && lastItemInColumnBottom <= containerBottom
144
+ if (!itemInView) {
145
+ container.value.scrollTo({
146
+ top: lastItemInColumnTop - 10,
147
+ behavior: 'smooth'
148
+ })
149
+ }
124
150
  }
125
151
  }
126
152
 
@@ -237,11 +263,19 @@ onMounted(async () => {
237
263
 
238
264
  columns.value = getColumnCount()
239
265
 
240
- paginationHistory.value = [props.loadAtPage]
241
-
242
- const response = await getContent(paginationHistory.value[0])
266
+ // For cursor-based pagination, loadAtPage can be null for the first request
267
+ const initialPage = props.loadAtPage
268
+ paginationHistory.value = [initialPage]
243
269
 
244
- paginationHistory.value.push(response.nextPage)
270
+ // Skip initial load if skipInitialLoad prop is true
271
+ if (!props.skipInitialLoad) {
272
+ const response = await getContent(paginationHistory.value[0])
273
+ paginationHistory.value.push(response.nextPage)
274
+ } else {
275
+ await nextTick()
276
+ // Just refresh the layout with any existing items
277
+ refreshLayout(masonry.value)
278
+ }
245
279
 
246
280
  isLoading.value = false
247
281
 
@@ -56,11 +56,12 @@ export default function calculateLayout(items, container, columnCount, options =
56
56
 
57
57
  const col = index % columnCount;
58
58
  const originalWidth = item.width;
59
- const originalHeight = item.height + footer + header;
59
+ const originalHeight = item.height;
60
60
 
61
61
  newItem.columnWidth = columnWidth;
62
62
  newItem.left = col * (columnWidth + gutterX);
63
- newItem.columnHeight = Math.round((columnWidth * originalHeight) / originalWidth);
63
+ newItem.imageHeight = Math.round((columnWidth * originalHeight) / originalWidth);
64
+ newItem.columnHeight = newItem.imageHeight + footer + header;
64
65
  newItem.top = columnHeights[col];
65
66
 
66
67
  columnHeights[col] += newItem.columnHeight + gutterY;
@@ -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
- })