@wyxos/vibe 1.6.27 → 1.6.29

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.
@@ -1,164 +1,164 @@
1
- import { ref, nextTick, type Ref, type ComputedRef } from 'vue'
2
- import calculateLayout from './calculateLayout'
3
- import { getColumnCount, calculateContainerHeight } from './masonryUtils'
4
-
5
- export interface UseMasonryLayoutOptions {
6
- masonry: Ref<any[]>
7
- useSwipeMode: ComputedRef<boolean>
8
- container: Ref<HTMLElement | null>
9
- columns: Ref<number>
10
- containerWidth: Ref<number>
11
- masonryContentHeight: Ref<number>
12
- layout: ComputedRef<any>
13
- fixedDimensions: Ref<{ width?: number; height?: number } | null>
14
- checkItemDimensions: (items: any[], context: string) => void
15
- }
16
-
17
- export function useMasonryLayout(options: UseMasonryLayoutOptions) {
18
- const {
19
- masonry,
20
- useSwipeMode,
21
- container,
22
- columns,
23
- containerWidth,
24
- masonryContentHeight,
25
- layout,
26
- fixedDimensions,
27
- checkItemDimensions
28
- } = options
29
-
30
- // Cache previous layout state for incremental updates
31
- let previousLayoutItems: any[] = []
32
-
33
- function calculateHeight(content: any[]) {
34
- const newHeight = calculateContainerHeight(content as any)
35
- let floor = 0
36
- if (container.value) {
37
- const { scrollTop, clientHeight } = container.value
38
- floor = scrollTop + clientHeight + 100
39
- }
40
- masonryContentHeight.value = Math.max(newHeight, floor)
41
- }
42
-
43
- function refreshLayout(items: any[]) {
44
- if (useSwipeMode.value) {
45
- // In swipe mode, no layout calculation needed - items are stacked vertically
46
- masonry.value = items as any
47
- return
48
- }
49
-
50
- // Always update masonry value, even if container isn't ready
51
- // This ensures items are added in tests and when container isn't available yet
52
- masonry.value = items as any
53
-
54
- if (!container.value) return
55
- // Developer diagnostics: warn when dimensions are invalid
56
- checkItemDimensions(items as any[], 'refreshLayout')
57
-
58
- // Optimization: For large arrays, check if we can do incremental update
59
- // Only works if items were removed from the end (common case)
60
- const canUseIncremental = items.length > 1000 &&
61
- previousLayoutItems.length > items.length &&
62
- previousLayoutItems.length - items.length < 100 // Only small removals
63
-
64
- if (canUseIncremental) {
65
- // Check if items were removed from the end (most common case)
66
- let removedFromEnd = true
67
- for (let i = 0; i < items.length; i++) {
68
- if (items[i]?.id !== previousLayoutItems[i]?.id) {
69
- removedFromEnd = false
70
- break
71
- }
72
- }
73
-
74
- if (removedFromEnd) {
75
- // Items removed from end - we can reuse previous positions for remaining items
76
- // Just update indices and recalculate height
77
- const itemsWithIndex = items.map((item, index) => ({
78
- ...previousLayoutItems[index],
79
- originalIndex: index
80
- }))
81
-
82
- // Recalculate height only
83
- calculateHeight(itemsWithIndex as any)
84
- masonry.value = itemsWithIndex
85
- previousLayoutItems = itemsWithIndex
86
- return
87
- }
88
- }
89
-
90
- // Full recalculation (fallback for all other cases)
91
- // Update original index to reflect current position in array
92
- // This ensures indices are correct after items are removed
93
- const itemsWithIndex = items.map((item, index) => ({
94
- ...item,
95
- originalIndex: index
96
- }))
97
-
98
- // When fixed dimensions are set, ensure container uses the fixed width for layout
99
- // This prevents gaps when the container's actual width differs from the fixed width
100
- const containerEl = container.value as HTMLElement
101
- if (fixedDimensions.value && fixedDimensions.value.width !== undefined) {
102
- // Temporarily set width to match fixed dimensions for accurate layout calculation
103
- const originalWidth = containerEl.style.width
104
- const originalBoxSizing = containerEl.style.boxSizing
105
- containerEl.style.boxSizing = 'border-box'
106
- containerEl.style.width = `${fixedDimensions.value.width}px`
107
- // Force reflow
108
- containerEl.offsetWidth
109
-
110
- const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
111
-
112
- // Restore original width
113
- containerEl.style.width = originalWidth
114
- containerEl.style.boxSizing = originalBoxSizing
115
-
116
- calculateHeight(content as any)
117
- masonry.value = content
118
- // Cache for next incremental update
119
- previousLayoutItems = content
120
- } else {
121
- const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
122
- calculateHeight(content as any)
123
- masonry.value = content
124
- // Cache for next incremental update
125
- previousLayoutItems = content
126
- }
127
- }
128
-
129
- function setFixedDimensions(
130
- dimensions: { width?: number; height?: number } | null,
131
- updateScrollProgress?: () => void
132
- ) {
133
- fixedDimensions.value = dimensions
134
- if (dimensions) {
135
- if (dimensions.width !== undefined) containerWidth.value = dimensions.width
136
- // Force layout refresh when dimensions change
137
- if (!useSwipeMode.value && container.value && masonry.value.length > 0) {
138
- // Use nextTick to ensure DOM has updated
139
- nextTick(() => {
140
- columns.value = getColumnCount(layout.value as any, containerWidth.value)
141
- refreshLayout(masonry.value as any)
142
- if (updateScrollProgress) {
143
- updateScrollProgress()
144
- }
145
- })
146
- }
147
- }
148
- // When clearing fixed dimensions, restore from wrapper
149
- // Note: wrapper is not available in this composable, so this needs to be handled by caller
150
- }
151
-
152
- function onResize() {
153
- columns.value = getColumnCount(layout.value as any, containerWidth.value)
154
- refreshLayout(masonry.value as any)
155
- }
156
-
157
- return {
158
- refreshLayout,
159
- setFixedDimensions,
160
- onResize,
161
- calculateHeight
162
- }
163
- }
164
-
1
+ import { ref, nextTick, type Ref, type ComputedRef } from 'vue'
2
+ import calculateLayout from './calculateLayout'
3
+ import { getColumnCount, calculateContainerHeight } from './masonryUtils'
4
+
5
+ export interface UseMasonryLayoutOptions {
6
+ masonry: Ref<any[]>
7
+ useSwipeMode: ComputedRef<boolean>
8
+ container: Ref<HTMLElement | null>
9
+ columns: Ref<number>
10
+ containerWidth: Ref<number>
11
+ masonryContentHeight: Ref<number>
12
+ layout: ComputedRef<any>
13
+ fixedDimensions: Ref<{ width?: number; height?: number } | null>
14
+ checkItemDimensions: (items: any[], context: string) => void
15
+ }
16
+
17
+ export function useMasonryLayout(options: UseMasonryLayoutOptions) {
18
+ const {
19
+ masonry,
20
+ useSwipeMode,
21
+ container,
22
+ columns,
23
+ containerWidth,
24
+ masonryContentHeight,
25
+ layout,
26
+ fixedDimensions,
27
+ checkItemDimensions
28
+ } = options
29
+
30
+ // Cache previous layout state for incremental updates
31
+ let previousLayoutItems: any[] = []
32
+
33
+ function calculateHeight(content: any[]) {
34
+ const newHeight = calculateContainerHeight(content as any)
35
+ let floor = 0
36
+ if (container.value) {
37
+ const { scrollTop, clientHeight } = container.value
38
+ floor = scrollTop + clientHeight + 100
39
+ }
40
+ masonryContentHeight.value = Math.max(newHeight, floor)
41
+ }
42
+
43
+ function refreshLayout(items: any[]) {
44
+ if (useSwipeMode.value) {
45
+ // In swipe mode, no layout calculation needed - items are stacked vertically
46
+ masonry.value = items as any
47
+ return
48
+ }
49
+
50
+ // Always update masonry value, even if container isn't ready
51
+ // This ensures items are added in tests and when container isn't available yet
52
+ masonry.value = items as any
53
+
54
+ if (!container.value) return
55
+ // Developer diagnostics: warn when dimensions are invalid
56
+ checkItemDimensions(items as any[], 'refreshLayout')
57
+
58
+ // Optimization: For large arrays, check if we can do incremental update
59
+ // Only works if items were removed from the end (common case)
60
+ const canUseIncremental = items.length > 1000 &&
61
+ previousLayoutItems.length > items.length &&
62
+ previousLayoutItems.length - items.length < 100 // Only small removals
63
+
64
+ if (canUseIncremental) {
65
+ // Check if items were removed from the end (most common case)
66
+ let removedFromEnd = true
67
+ for (let i = 0; i < items.length; i++) {
68
+ if (items[i]?.id !== previousLayoutItems[i]?.id) {
69
+ removedFromEnd = false
70
+ break
71
+ }
72
+ }
73
+
74
+ if (removedFromEnd) {
75
+ // Items removed from end - we can reuse previous positions for remaining items
76
+ // Just update indices and recalculate height
77
+ const itemsWithIndex = items.map((item, index) => ({
78
+ ...previousLayoutItems[index],
79
+ originalIndex: index
80
+ }))
81
+
82
+ // Recalculate height only
83
+ calculateHeight(itemsWithIndex as any)
84
+ masonry.value = itemsWithIndex
85
+ previousLayoutItems = itemsWithIndex
86
+ return
87
+ }
88
+ }
89
+
90
+ // Full recalculation (fallback for all other cases)
91
+ // Update original index to reflect current position in array
92
+ // This ensures indices are correct after items are removed
93
+ const itemsWithIndex = items.map((item, index) => ({
94
+ ...item,
95
+ originalIndex: index
96
+ }))
97
+
98
+ // When fixed dimensions are set, ensure container uses the fixed width for layout
99
+ // This prevents gaps when the container's actual width differs from the fixed width
100
+ const containerEl = container.value as HTMLElement
101
+ if (fixedDimensions.value && fixedDimensions.value.width !== undefined) {
102
+ // Temporarily set width to match fixed dimensions for accurate layout calculation
103
+ const originalWidth = containerEl.style.width
104
+ const originalBoxSizing = containerEl.style.boxSizing
105
+ containerEl.style.boxSizing = 'border-box'
106
+ containerEl.style.width = `${fixedDimensions.value.width}px`
107
+ // Force reflow
108
+ containerEl.offsetWidth
109
+
110
+ const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
111
+
112
+ // Restore original width
113
+ containerEl.style.width = originalWidth
114
+ containerEl.style.boxSizing = originalBoxSizing
115
+
116
+ calculateHeight(content as any)
117
+ masonry.value = content
118
+ // Cache for next incremental update
119
+ previousLayoutItems = content
120
+ } else {
121
+ const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
122
+ calculateHeight(content as any)
123
+ masonry.value = content
124
+ // Cache for next incremental update
125
+ previousLayoutItems = content
126
+ }
127
+ }
128
+
129
+ function setFixedDimensions(
130
+ dimensions: { width?: number; height?: number } | null,
131
+ updateScrollProgress?: () => void
132
+ ) {
133
+ fixedDimensions.value = dimensions
134
+ if (dimensions) {
135
+ if (dimensions.width !== undefined) containerWidth.value = dimensions.width
136
+ // Force layout refresh when dimensions change
137
+ if (!useSwipeMode.value && container.value && masonry.value.length > 0) {
138
+ // Use nextTick to ensure DOM has updated
139
+ nextTick(() => {
140
+ columns.value = getColumnCount(layout.value as any, containerWidth.value)
141
+ refreshLayout(masonry.value as any)
142
+ if (updateScrollProgress) {
143
+ updateScrollProgress()
144
+ }
145
+ })
146
+ }
147
+ }
148
+ // When clearing fixed dimensions, restore from wrapper
149
+ // Note: wrapper is not available in this composable, so this needs to be handled by caller
150
+ }
151
+
152
+ function onResize() {
153
+ columns.value = getColumnCount(layout.value as any, containerWidth.value)
154
+ refreshLayout(masonry.value as any)
155
+ }
156
+
157
+ return {
158
+ refreshLayout,
159
+ setFixedDimensions,
160
+ onResize,
161
+ calculateHeight
162
+ }
163
+ }
164
+
@@ -1,4 +1,4 @@
1
- import { ref, type Ref } from 'vue'
1
+ import { ref, nextTick, type Ref } from 'vue'
2
2
  import { normalizeError } from './utils/errorHandler'
3
3
 
4
4
  export interface UseMasonryPaginationOptions {
@@ -14,7 +14,7 @@ export interface UseMasonryPaginationOptions {
14
14
  retryMaxAttempts: number
15
15
  retryInitialDelayMs: number
16
16
  retryBackoffStepMs: number
17
- mode: string
17
+ mode: string | Ref<string>
18
18
  backfillDelayMs: number
19
19
  backfillMaxCalls: number
20
20
  pageSize: number
@@ -23,10 +23,10 @@ export interface UseMasonryPaginationOptions {
23
23
  (event: 'retry:start', payload: { attempt: number; max: number; totalMs: number }): void
24
24
  (event: 'retry:tick', payload: { attempt: number; remainingMs: number; totalMs: number }): void
25
25
  (event: 'retry:stop', payload: { attempt: number; success: boolean }): void
26
- (event: 'backfill:start', payload: { target: number; fetched: number; calls: number }): void
27
- (event: 'backfill:tick', payload: { fetched: number; target: number; calls: number; remainingMs: number; totalMs: number }): void
28
- (event: 'backfill:stop', payload: { fetched: number; calls: number; cancelled?: boolean }): void
29
- (event: 'loading:stop', payload: { fetched: number }): void
26
+ (event: 'backfill:start', payload: { target: number; fetched: number; calls: number; currentPage: any; nextPage: any }): void
27
+ (event: 'backfill:tick', payload: { fetched: number; target: number; calls: number; remainingMs: number; totalMs: number; currentPage: any; nextPage: any }): void
28
+ (event: 'backfill:stop', payload: { fetched: number; calls: number; cancelled?: boolean; currentPage: any; nextPage: any }): void
29
+ (event: 'loading:stop', payload: { fetched: number; currentPage: any; nextPage: any }): void
30
30
  }
31
31
  }
32
32
 
@@ -51,6 +51,9 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
51
51
  emits
52
52
  } = options
53
53
 
54
+ // Make mode reactive so it updates when the prop changes
55
+ const modeRef = typeof mode === 'string' ? ref(mode) : (mode as Ref<string>)
56
+
54
57
  const cancelRequested = ref(false)
55
58
  let backfillActive = false
56
59
 
@@ -135,9 +138,18 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
135
138
 
136
139
  async function getContent(page: number) {
137
140
  try {
138
- const response = await fetchWithRetry(() => getPage(page, context?.value))
139
- refreshLayout([...masonry.value, ...response.items])
140
- return response
141
+ const pageData = await fetchWithRetry(() => getPage(page, context?.value))
142
+ // Add items to masonry array first (allows Vue transition-group to detect new items)
143
+ const newItems = [...masonry.value, ...pageData.items]
144
+ masonry.value = newItems
145
+ await nextTick()
146
+
147
+ // Commit DOM updates without forcing sync reflow
148
+ await nextTick()
149
+ // Start FLIP on next tick (same pattern as restore/restoreMany)
150
+ await nextTick()
151
+ refreshLayout(newItems)
152
+ return pageData
141
153
  } catch (error) {
142
154
  // Error is handled by callers (loadPage, loadNext, etc.) which set loadError
143
155
  throw error
@@ -145,7 +157,7 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
145
157
  }
146
158
 
147
159
  async function maybeBackfillToTarget(baselineCount: number, force = false) {
148
- if (!force && mode !== 'backfill') return
160
+ if (!force && modeRef.value !== 'backfill') return
149
161
  if (backfillActive) return
150
162
  if (cancelRequested.value) return
151
163
  // Don't backfill if we've reached the end
@@ -170,7 +182,15 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
170
182
  }
171
183
  try {
172
184
  let calls = 0
173
- emits('backfill:start', { target: targetCount, fetched: masonry.value.length, calls })
185
+ const initialCurrentPage = currentPage.value
186
+ const initialNextPage = paginationHistory.value[paginationHistory.value.length - 1]
187
+ emits('backfill:start', {
188
+ target: targetCount,
189
+ fetched: masonry.value.length,
190
+ calls,
191
+ currentPage: initialCurrentPage,
192
+ nextPage: initialNextPage
193
+ })
174
194
 
175
195
  while (
176
196
  masonry.value.length < targetCount &&
@@ -180,20 +200,24 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
180
200
  !hasReachedEnd.value &&
181
201
  backfillActive
182
202
  ) {
203
+ const tickCurrentPage = currentPage.value
204
+ const tickNextPage = paginationHistory.value[paginationHistory.value.length - 1]
183
205
  await waitWithProgress(backfillDelayMs, (remaining, total) => {
184
206
  emits('backfill:tick', {
185
207
  fetched: masonry.value.length,
186
208
  target: targetCount,
187
209
  calls,
188
210
  remainingMs: remaining,
189
- totalMs: total
211
+ totalMs: total,
212
+ currentPage: tickCurrentPage,
213
+ nextPage: tickNextPage
190
214
  })
191
215
  })
192
216
 
193
217
  if (cancelRequested.value || !backfillActive) break
194
218
 
195
- const currentPage = paginationHistory.value[paginationHistory.value.length - 1]
196
- if (currentPage == null) {
219
+ const currentPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
220
+ if (currentPageToLoad == null) {
197
221
  hasReachedEnd.value = true
198
222
  break
199
223
  }
@@ -201,13 +225,14 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
201
225
  // Don't toggle isLoading here - keep it true throughout backfill
202
226
  // Check cancellation before starting getContent to avoid unnecessary requests
203
227
  if (cancelRequested.value || !backfillActive) break
204
- const response = await getContent(currentPage)
228
+ const pageData = await getContent(currentPageToLoad)
205
229
  if (cancelRequested.value || !backfillActive) break
206
230
  // Clear error on successful load
207
231
  loadError.value = null
208
- paginationHistory.value.push(response.nextPage)
232
+ currentPage.value = currentPageToLoad
233
+ paginationHistory.value.push(pageData.nextPage)
209
234
  // Update hasReachedEnd if nextPage is null
210
- if (response.nextPage == null) {
235
+ if (pageData.nextPage == null) {
211
236
  hasReachedEnd.value = true
212
237
  }
213
238
  } catch (error) {
@@ -219,12 +244,25 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
219
244
  calls++
220
245
  }
221
246
 
222
- emits('backfill:stop', { fetched: masonry.value.length, calls })
247
+ const stopCurrentPage = currentPage.value
248
+ const stopNextPage = paginationHistory.value[paginationHistory.value.length - 1]
249
+ emits('backfill:stop', {
250
+ fetched: masonry.value.length,
251
+ calls,
252
+ currentPage: stopCurrentPage,
253
+ nextPage: stopNextPage
254
+ })
223
255
  } finally {
224
256
  backfillActive = false
225
257
  // Only set loading to false when backfill completes or is cancelled
226
258
  isLoading.value = false
227
- emits('loading:stop', { fetched: masonry.value.length })
259
+ const finalCurrentPage = currentPage.value
260
+ const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
261
+ emits('loading:stop', {
262
+ fetched: masonry.value.length,
263
+ currentPage: finalCurrentPage,
264
+ nextPage: finalNextPage
265
+ })
228
266
  }
229
267
  }
230
268
 
@@ -242,24 +280,31 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
242
280
  try {
243
281
  const baseline = masonry.value.length
244
282
  if (cancelRequested.value) return
245
- const response = await getContent(page)
283
+ const pageData = await getContent(page)
246
284
  if (cancelRequested.value) return
247
285
  // Clear error on successful load
248
286
  loadError.value = null
249
287
  currentPage.value = page // Track the current page
250
- paginationHistory.value.push(response.nextPage)
288
+ paginationHistory.value.push(pageData.nextPage)
251
289
  // Update hasReachedEnd if nextPage is null
252
- if (response.nextPage == null) {
290
+ if (pageData.nextPage == null) {
253
291
  hasReachedEnd.value = true
254
292
  }
255
293
  await maybeBackfillToTarget(baseline)
256
- return response
294
+ return pageData
257
295
  } catch (error) {
258
296
  // Set load error - error is handled and exposed to UI via loadError
259
297
  loadError.value = normalizeError(error)
260
298
  throw error
261
299
  } finally {
262
300
  isLoading.value = false
301
+ const finalCurrentPage = currentPage.value
302
+ const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
303
+ emits('loading:stop', {
304
+ fetched: masonry.value.length,
305
+ currentPage: finalCurrentPage,
306
+ nextPage: finalNextPage
307
+ })
263
308
  }
264
309
  }
265
310
 
@@ -280,18 +325,18 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
280
325
  if (cancelRequested.value) return
281
326
 
282
327
  // Refresh mode: check if current page needs refreshing before loading next
283
- if (mode === 'refresh' && currentPage.value != null) {
328
+ if (modeRef.value === 'refresh' && currentPage.value != null) {
284
329
  const currentPageItemCount = countItemsForPage(currentPage.value)
285
330
 
286
331
  // If current page has fewer items than pageSize, refresh it first
287
332
  if (currentPageItemCount < pageSize) {
288
- const response = await fetchWithRetry(() => getPage(currentPage.value, context?.value))
333
+ const pageData = await fetchWithRetry(() => getPage(currentPage.value, context?.value))
289
334
  if (cancelRequested.value) return
290
335
 
291
336
  // Get only new items that don't already exist
292
337
  // We need to check against the current masonry state at this moment
293
338
  const currentMasonrySnapshot = [...masonry.value]
294
- const newItems = response.items.filter((item: any) => {
339
+ const newItems = pageData.items.filter((item: any) => {
295
340
  if (!item || item.id == null || item.page == null) return false
296
341
  return !currentMasonrySnapshot.some((existing: any) => {
297
342
  return existing && existing.id === item.id && existing.page === item.page
@@ -301,9 +346,14 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
301
346
  // Append only new items to masonry (same pattern as getContent)
302
347
  if (newItems.length > 0) {
303
348
  const updatedItems = [...masonry.value, ...newItems]
349
+ masonry.value = updatedItems
350
+ await nextTick()
351
+
352
+ // Commit DOM updates without forcing sync reflow
353
+ await nextTick()
354
+ // Start FLIP on next tick (same pattern as restore/restoreMany)
355
+ await nextTick()
304
356
  refreshLayout(updatedItems)
305
- // Wait a tick for masonry to update
306
- await new Promise(resolve => setTimeout(resolve, 0))
307
357
  }
308
358
 
309
359
  // Clear error on successful load
@@ -350,8 +400,8 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
350
400
  await maybeBackfillToTarget(baseline)
351
401
  return nextResponse
352
402
  } else {
353
- // Still not enough items, but we refreshed - return the refresh response
354
- return response
403
+ // Still not enough items, but we refreshed - return the refresh pageData
404
+ return pageData
355
405
  }
356
406
  }
357
407
  }
@@ -363,25 +413,31 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
363
413
  hasReachedEnd.value = true
364
414
  return
365
415
  }
366
- const response = await getContent(nextPageToLoad)
416
+ const pageData = await getContent(nextPageToLoad)
367
417
  if (cancelRequested.value) return
368
418
  // Clear error on successful load
369
419
  loadError.value = null
370
420
  currentPage.value = nextPageToLoad // Track the current page
371
- paginationHistory.value.push(response.nextPage)
421
+ paginationHistory.value.push(pageData.nextPage)
372
422
  // Update hasReachedEnd if nextPage is null
373
- if (response.nextPage == null) {
423
+ if (pageData.nextPage == null) {
374
424
  hasReachedEnd.value = true
375
425
  }
376
426
  await maybeBackfillToTarget(baseline)
377
- return response
427
+ return pageData
378
428
  } catch (error) {
379
429
  // Set load error - error is handled and exposed to UI via loadError
380
430
  loadError.value = normalizeError(error)
381
431
  throw error
382
432
  } finally {
383
433
  isLoading.value = false
384
- emits('loading:stop', { fetched: masonry.value.length })
434
+ const finalCurrentPage = currentPage.value
435
+ const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
436
+ emits('loading:stop', {
437
+ fetched: masonry.value.length,
438
+ currentPage: finalCurrentPage,
439
+ nextPage: finalNextPage
440
+ })
385
441
  }
386
442
  }
387
443
 
@@ -411,16 +467,16 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
411
467
  paginationHistory.value = [pageToRefresh]
412
468
 
413
469
  // Reload the current page
414
- const response = await getContent(pageToRefresh)
470
+ const pageData = await getContent(pageToRefresh)
415
471
  if (cancelRequested.value) return
416
472
 
417
473
  // Clear error on successful load
418
474
  loadError.value = null
419
475
  // Update pagination state
420
476
  currentPage.value = pageToRefresh
421
- paginationHistory.value.push(response.nextPage)
477
+ paginationHistory.value.push(pageData.nextPage)
422
478
  // Update hasReachedEnd if nextPage is null
423
- if (response.nextPage == null) {
479
+ if (pageData.nextPage == null) {
424
480
  hasReachedEnd.value = true
425
481
  }
426
482
 
@@ -428,14 +484,20 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
428
484
  const baseline = masonry.value.length
429
485
  await maybeBackfillToTarget(baseline)
430
486
 
431
- return response
487
+ return pageData
432
488
  } catch (error) {
433
489
  // Set load error - error is handled and exposed to UI via loadError
434
490
  loadError.value = normalizeError(error)
435
491
  throw error
436
492
  } finally {
437
493
  isLoading.value = false
438
- emits('loading:stop', { fetched: masonry.value.length })
494
+ const finalCurrentPage = currentPage.value
495
+ const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
496
+ emits('loading:stop', {
497
+ fetched: masonry.value.length,
498
+ currentPage: finalCurrentPage,
499
+ nextPage: finalNextPage
500
+ })
439
501
  }
440
502
  }
441
503
 
@@ -446,11 +508,23 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
446
508
  // Set backfillActive to false to immediately stop backfilling
447
509
  // The backfill loop checks this flag and will exit on the next iteration
448
510
  backfillActive = false
511
+ const cancelCurrentPage = currentPage.value
512
+ const cancelNextPage = paginationHistory.value[paginationHistory.value.length - 1]
449
513
  // If backfill was active, emit stop event immediately
450
514
  if (wasBackfilling) {
451
- emits('backfill:stop', { fetched: masonry.value.length, calls: 0, cancelled: true })
515
+ emits('backfill:stop', {
516
+ fetched: masonry.value.length,
517
+ calls: 0,
518
+ cancelled: true,
519
+ currentPage: cancelCurrentPage,
520
+ nextPage: cancelNextPage
521
+ })
452
522
  }
453
- emits('loading:stop', { fetched: masonry.value.length })
523
+ emits('loading:stop', {
524
+ fetched: masonry.value.length,
525
+ currentPage: cancelCurrentPage,
526
+ nextPage: cancelNextPage
527
+ })
454
528
  }
455
529
 
456
530
  return {