@wyxos/vibe 1.6.29 → 2.0.2

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.
Files changed (43) hide show
  1. package/README.md +29 -287
  2. package/lib/index.cjs +1 -0
  3. package/lib/index.js +795 -1791
  4. package/lib/logo-dark.svg +36 -36
  5. package/lib/logo-light.svg +29 -29
  6. package/lib/logo.svg +32 -32
  7. package/lib/manifest.json +1 -1
  8. package/package.json +82 -96
  9. package/LICENSE +0 -21
  10. package/lib/vibe.css +0 -1
  11. package/lib/vite.svg +0 -1
  12. package/src/App.vue +0 -35
  13. package/src/Masonry.vue +0 -1030
  14. package/src/archive/App.vue +0 -96
  15. package/src/archive/InfiniteMansonry.spec.ts +0 -10
  16. package/src/archive/InfiniteMasonry.vue +0 -218
  17. package/src/assets/vue.svg +0 -1
  18. package/src/calculateLayout.ts +0 -194
  19. package/src/components/CodeTabs.vue +0 -158
  20. package/src/components/MasonryItem.vue +0 -499
  21. package/src/components/examples/BasicExample.vue +0 -46
  22. package/src/components/examples/CustomItemExample.vue +0 -87
  23. package/src/components/examples/HeaderFooterExample.vue +0 -79
  24. package/src/components/examples/ManualInitExample.vue +0 -78
  25. package/src/components/examples/SwipeModeExample.vue +0 -40
  26. package/src/createMasonryTransitions.ts +0 -176
  27. package/src/main.ts +0 -6
  28. package/src/masonryUtils.ts +0 -96
  29. package/src/pages.json +0 -36402
  30. package/src/router/index.ts +0 -20
  31. package/src/style.css +0 -32
  32. package/src/types.ts +0 -101
  33. package/src/useMasonryDimensions.ts +0 -59
  34. package/src/useMasonryItems.ts +0 -231
  35. package/src/useMasonryLayout.ts +0 -164
  36. package/src/useMasonryPagination.ts +0 -539
  37. package/src/useMasonryScroll.ts +0 -61
  38. package/src/useMasonryVirtualization.ts +0 -140
  39. package/src/useSwipeMode.ts +0 -233
  40. package/src/utils/errorHandler.ts +0 -8
  41. package/src/views/Examples.vue +0 -323
  42. package/src/views/Home.vue +0 -321
  43. package/toggle-link.mjs +0 -92
@@ -1,539 +0,0 @@
1
- import { ref, nextTick, type Ref } from 'vue'
2
- import { normalizeError } from './utils/errorHandler'
3
-
4
- export interface UseMasonryPaginationOptions {
5
- getPage: (page: any, context?: any) => Promise<{ items: any[]; nextPage: any }>
6
- context?: Ref<any>
7
- masonry: Ref<any[]>
8
- isLoading: Ref<boolean>
9
- hasReachedEnd: Ref<boolean>
10
- loadError: Ref<Error | null>
11
- currentPage: Ref<any>
12
- paginationHistory: Ref<any[]>
13
- refreshLayout: (items: any[]) => void
14
- retryMaxAttempts: number
15
- retryInitialDelayMs: number
16
- retryBackoffStepMs: number
17
- mode: string | Ref<string>
18
- backfillDelayMs: number
19
- backfillMaxCalls: number
20
- pageSize: number
21
- emits: {
22
- (event: 'loading:start'): void
23
- (event: 'retry:start', payload: { attempt: number; max: number; totalMs: number }): void
24
- (event: 'retry:tick', payload: { attempt: number; remainingMs: number; totalMs: number }): void
25
- (event: 'retry:stop', payload: { attempt: number; success: boolean }): 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
- }
31
- }
32
-
33
- export function useMasonryPagination(options: UseMasonryPaginationOptions) {
34
- const {
35
- getPage,
36
- context,
37
- masonry,
38
- isLoading,
39
- hasReachedEnd,
40
- loadError,
41
- currentPage,
42
- paginationHistory,
43
- refreshLayout,
44
- retryMaxAttempts,
45
- retryInitialDelayMs,
46
- retryBackoffStepMs,
47
- mode,
48
- backfillDelayMs,
49
- backfillMaxCalls,
50
- pageSize,
51
- emits
52
- } = options
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
-
57
- const cancelRequested = ref(false)
58
- let backfillActive = false
59
-
60
- // Helper function to count items for a specific page
61
- function countItemsForPage(page: any): number {
62
- return masonry.value.filter((item: any) => item.page === page).length
63
- }
64
-
65
- // Helper function to check if an item already exists in masonry
66
- function itemExists(item: any, itemsArray?: any[]): boolean {
67
- if (!item || item.id == null || item.page == null) return false
68
- const itemsToCheck = itemsArray || masonry.value
69
- return itemsToCheck.some((existing: any) => {
70
- return existing && existing.id === item.id && existing.page === item.page
71
- })
72
- }
73
-
74
- // Helper function to get only new items from a response
75
- function getNewItems(responseItems: any[]): any[] {
76
- if (!responseItems || responseItems.length === 0) return []
77
- // Create a snapshot of current masonry items to avoid reactivity issues
78
- const currentItems = [...masonry.value]
79
- return responseItems.filter((item: any) => {
80
- if (!item || item.id == null || item.page == null) return false
81
- // Check if item exists by comparing id and page
82
- const exists = currentItems.some((existing: any) => {
83
- return existing && existing.id === item.id && existing.page === item.page
84
- })
85
- return !exists
86
- })
87
- }
88
-
89
- function waitWithProgress(totalMs: number, onTick: (remaining: number, total: number) => void) {
90
- return new Promise<void>((resolve) => {
91
- const total = Math.max(0, totalMs | 0)
92
- const start = Date.now()
93
- onTick(total, total)
94
- const id = setInterval(() => {
95
- // Check for cancellation
96
- if (cancelRequested.value) {
97
- clearInterval(id)
98
- resolve()
99
- return
100
- }
101
- const elapsed = Date.now() - start
102
- const remaining = Math.max(0, total - elapsed)
103
- onTick(remaining, total)
104
- if (remaining <= 0) {
105
- clearInterval(id)
106
- resolve()
107
- }
108
- }, 100)
109
- })
110
- }
111
-
112
- async function fetchWithRetry<T = any>(fn: () => Promise<T>): Promise<T> {
113
- let attempt = 0
114
- const max = retryMaxAttempts
115
- let delay = retryInitialDelayMs
116
- // eslint-disable-next-line no-constant-condition
117
- while (true) {
118
- try {
119
- const res = await fn()
120
- if (attempt > 0) {
121
- emits('retry:stop', { attempt, success: true })
122
- }
123
- return res
124
- } catch (err) {
125
- attempt++
126
- if (attempt > max) {
127
- emits('retry:stop', { attempt: attempt - 1, success: false })
128
- throw err
129
- }
130
- emits('retry:start', { attempt, max, totalMs: delay })
131
- await waitWithProgress(delay, (remaining, total) => {
132
- emits('retry:tick', { attempt, remainingMs: remaining, totalMs: total })
133
- })
134
- delay += retryBackoffStepMs
135
- }
136
- }
137
- }
138
-
139
- async function getContent(page: number) {
140
- try {
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
153
- } catch (error) {
154
- // Error is handled by callers (loadPage, loadNext, etc.) which set loadError
155
- throw error
156
- }
157
- }
158
-
159
- async function maybeBackfillToTarget(baselineCount: number, force = false) {
160
- if (!force && modeRef.value !== 'backfill') return
161
- if (backfillActive) return
162
- if (cancelRequested.value) return
163
- // Don't backfill if we've reached the end
164
- if (hasReachedEnd.value) return
165
-
166
- const targetCount = (baselineCount || 0) + (pageSize || 0)
167
- if (!pageSize || pageSize <= 0) return
168
-
169
- const lastNext = paginationHistory.value[paginationHistory.value.length - 1]
170
- if (lastNext == null) {
171
- hasReachedEnd.value = true
172
- return
173
- }
174
-
175
- if (masonry.value.length >= targetCount) return
176
-
177
- backfillActive = true
178
- // Set loading to true at the start of backfill and keep it true throughout
179
- if (!isLoading.value) {
180
- isLoading.value = true
181
- emits('loading:start')
182
- }
183
- try {
184
- let calls = 0
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
- })
194
-
195
- while (
196
- masonry.value.length < targetCount &&
197
- calls < backfillMaxCalls &&
198
- paginationHistory.value[paginationHistory.value.length - 1] != null &&
199
- !cancelRequested.value &&
200
- !hasReachedEnd.value &&
201
- backfillActive
202
- ) {
203
- const tickCurrentPage = currentPage.value
204
- const tickNextPage = paginationHistory.value[paginationHistory.value.length - 1]
205
- await waitWithProgress(backfillDelayMs, (remaining, total) => {
206
- emits('backfill:tick', {
207
- fetched: masonry.value.length,
208
- target: targetCount,
209
- calls,
210
- remainingMs: remaining,
211
- totalMs: total,
212
- currentPage: tickCurrentPage,
213
- nextPage: tickNextPage
214
- })
215
- })
216
-
217
- if (cancelRequested.value || !backfillActive) break
218
-
219
- const currentPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
220
- if (currentPageToLoad == null) {
221
- hasReachedEnd.value = true
222
- break
223
- }
224
- try {
225
- // Don't toggle isLoading here - keep it true throughout backfill
226
- // Check cancellation before starting getContent to avoid unnecessary requests
227
- if (cancelRequested.value || !backfillActive) break
228
- const pageData = await getContent(currentPageToLoad)
229
- if (cancelRequested.value || !backfillActive) break
230
- // Clear error on successful load
231
- loadError.value = null
232
- currentPage.value = currentPageToLoad
233
- paginationHistory.value.push(pageData.nextPage)
234
- // Update hasReachedEnd if nextPage is null
235
- if (pageData.nextPage == null) {
236
- hasReachedEnd.value = true
237
- }
238
- } catch (error) {
239
- // Set load error but don't break the backfill loop unless cancelled
240
- if (cancelRequested.value || !backfillActive) break
241
- loadError.value = normalizeError(error)
242
- }
243
-
244
- calls++
245
- }
246
-
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
- })
255
- } finally {
256
- backfillActive = false
257
- // Only set loading to false when backfill completes or is cancelled
258
- isLoading.value = false
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
- })
266
- }
267
- }
268
-
269
- async function loadPage(page: number) {
270
- if (isLoading.value) return
271
- // Starting a new load should clear any previous cancel request
272
- cancelRequested.value = false
273
- if (!isLoading.value) {
274
- isLoading.value = true
275
- emits('loading:start')
276
- }
277
- // Reset hasReachedEnd and loadError when loading a new page
278
- hasReachedEnd.value = false
279
- loadError.value = null
280
- try {
281
- const baseline = masonry.value.length
282
- if (cancelRequested.value) return
283
- const pageData = await getContent(page)
284
- if (cancelRequested.value) return
285
- // Clear error on successful load
286
- loadError.value = null
287
- currentPage.value = page // Track the current page
288
- paginationHistory.value.push(pageData.nextPage)
289
- // Update hasReachedEnd if nextPage is null
290
- if (pageData.nextPage == null) {
291
- hasReachedEnd.value = true
292
- }
293
- await maybeBackfillToTarget(baseline)
294
- return pageData
295
- } catch (error) {
296
- // Set load error - error is handled and exposed to UI via loadError
297
- loadError.value = normalizeError(error)
298
- throw error
299
- } finally {
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
- })
308
- }
309
- }
310
-
311
- async function loadNext() {
312
- if (isLoading.value) return
313
- // Don't load if we've already reached the end
314
- if (hasReachedEnd.value) return
315
- // Starting a new load should clear any previous cancel request
316
- cancelRequested.value = false
317
- if (!isLoading.value) {
318
- isLoading.value = true
319
- emits('loading:start')
320
- }
321
- // Clear error when attempting to load
322
- loadError.value = null
323
- try {
324
- const baseline = masonry.value.length
325
- if (cancelRequested.value) return
326
-
327
- // Refresh mode: check if current page needs refreshing before loading next
328
- if (modeRef.value === 'refresh' && currentPage.value != null) {
329
- const currentPageItemCount = countItemsForPage(currentPage.value)
330
-
331
- // If current page has fewer items than pageSize, refresh it first
332
- if (currentPageItemCount < pageSize) {
333
- const pageData = await fetchWithRetry(() => getPage(currentPage.value, context?.value))
334
- if (cancelRequested.value) return
335
-
336
- // Get only new items that don't already exist
337
- // We need to check against the current masonry state at this moment
338
- const currentMasonrySnapshot = [...masonry.value]
339
- const newItems = pageData.items.filter((item: any) => {
340
- if (!item || item.id == null || item.page == null) return false
341
- return !currentMasonrySnapshot.some((existing: any) => {
342
- return existing && existing.id === item.id && existing.page === item.page
343
- })
344
- })
345
-
346
- // Append only new items to masonry (same pattern as getContent)
347
- if (newItems.length > 0) {
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()
356
- refreshLayout(updatedItems)
357
- }
358
-
359
- // Clear error on successful load
360
- loadError.value = null
361
-
362
- // If no new items were found, automatically proceed to next page
363
- // This means the current page has no more items available
364
- if (newItems.length === 0) {
365
- const nextPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
366
- if (nextPageToLoad == null) {
367
- hasReachedEnd.value = true
368
- return
369
- }
370
- const nextResponse = await getContent(nextPageToLoad)
371
- if (cancelRequested.value) return
372
- loadError.value = null
373
- currentPage.value = nextPageToLoad
374
- paginationHistory.value.push(nextResponse.nextPage)
375
- if (nextResponse.nextPage == null) {
376
- hasReachedEnd.value = true
377
- }
378
- await maybeBackfillToTarget(baseline)
379
- return nextResponse
380
- }
381
-
382
- // If we now have enough items for current page, proceed to next page
383
- // Re-check count after items have been added
384
- const updatedCount = countItemsForPage(currentPage.value)
385
- if (updatedCount >= pageSize) {
386
- // Current page is now full, proceed with normal next page loading
387
- const nextPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
388
- if (nextPageToLoad == null) {
389
- hasReachedEnd.value = true
390
- return
391
- }
392
- const nextResponse = await getContent(nextPageToLoad)
393
- if (cancelRequested.value) return
394
- loadError.value = null
395
- currentPage.value = nextPageToLoad
396
- paginationHistory.value.push(nextResponse.nextPage)
397
- if (nextResponse.nextPage == null) {
398
- hasReachedEnd.value = true
399
- }
400
- await maybeBackfillToTarget(baseline)
401
- return nextResponse
402
- } else {
403
- // Still not enough items, but we refreshed - return the refresh pageData
404
- return pageData
405
- }
406
- }
407
- }
408
-
409
- // Normal flow: load next page
410
- const nextPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
411
- // Don't load if nextPageToLoad is null
412
- if (nextPageToLoad == null) {
413
- hasReachedEnd.value = true
414
- return
415
- }
416
- const pageData = await getContent(nextPageToLoad)
417
- if (cancelRequested.value) return
418
- // Clear error on successful load
419
- loadError.value = null
420
- currentPage.value = nextPageToLoad // Track the current page
421
- paginationHistory.value.push(pageData.nextPage)
422
- // Update hasReachedEnd if nextPage is null
423
- if (pageData.nextPage == null) {
424
- hasReachedEnd.value = true
425
- }
426
- await maybeBackfillToTarget(baseline)
427
- return pageData
428
- } catch (error) {
429
- // Set load error - error is handled and exposed to UI via loadError
430
- loadError.value = normalizeError(error)
431
- throw error
432
- } finally {
433
- isLoading.value = false
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
- })
441
- }
442
- }
443
-
444
- async function refreshCurrentPage() {
445
- if (isLoading.value) return
446
- cancelRequested.value = false
447
- isLoading.value = true
448
- emits('loading:start')
449
-
450
- try {
451
- // Use the tracked current page
452
- const pageToRefresh = currentPage.value
453
-
454
- if (pageToRefresh == null) {
455
- console.warn('[Masonry] No current page to refresh - currentPage:', currentPage.value, 'paginationHistory:', paginationHistory.value)
456
- return
457
- }
458
-
459
- // Clear existing items
460
- masonry.value = []
461
- // Reset end flag when refreshing
462
- hasReachedEnd.value = false
463
- // Reset error flag when refreshing
464
- loadError.value = null
465
-
466
- // Reset pagination history to just the current page
467
- paginationHistory.value = [pageToRefresh]
468
-
469
- // Reload the current page
470
- const pageData = await getContent(pageToRefresh)
471
- if (cancelRequested.value) return
472
-
473
- // Clear error on successful load
474
- loadError.value = null
475
- // Update pagination state
476
- currentPage.value = pageToRefresh
477
- paginationHistory.value.push(pageData.nextPage)
478
- // Update hasReachedEnd if nextPage is null
479
- if (pageData.nextPage == null) {
480
- hasReachedEnd.value = true
481
- }
482
-
483
- // Optionally backfill if needed
484
- const baseline = masonry.value.length
485
- await maybeBackfillToTarget(baseline)
486
-
487
- return pageData
488
- } catch (error) {
489
- // Set load error - error is handled and exposed to UI via loadError
490
- loadError.value = normalizeError(error)
491
- throw error
492
- } finally {
493
- isLoading.value = false
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
- })
501
- }
502
- }
503
-
504
- function cancelLoad() {
505
- const wasBackfilling = backfillActive
506
- cancelRequested.value = true
507
- isLoading.value = false
508
- // Set backfillActive to false to immediately stop backfilling
509
- // The backfill loop checks this flag and will exit on the next iteration
510
- backfillActive = false
511
- const cancelCurrentPage = currentPage.value
512
- const cancelNextPage = paginationHistory.value[paginationHistory.value.length - 1]
513
- // If backfill was active, emit stop event immediately
514
- if (wasBackfilling) {
515
- emits('backfill:stop', {
516
- fetched: masonry.value.length,
517
- calls: 0,
518
- cancelled: true,
519
- currentPage: cancelCurrentPage,
520
- nextPage: cancelNextPage
521
- })
522
- }
523
- emits('loading:stop', {
524
- fetched: masonry.value.length,
525
- currentPage: cancelCurrentPage,
526
- nextPage: cancelNextPage
527
- })
528
- }
529
-
530
- return {
531
- loadPage,
532
- loadNext,
533
- refreshCurrentPage,
534
- cancelLoad,
535
- maybeBackfillToTarget,
536
- getContent
537
- }
538
- }
539
-
@@ -1,61 +0,0 @@
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[], forceCheck = false) {
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
- // Allow loading if near bottom and either scrolling down OR forceCheck is true (for restoration)
51
- if (nearBottom && (isScrollingDown || forceCheck) && !isLoading.value) {
52
- await loadNext()
53
- await nextTick()
54
- return
55
- }
56
- }
57
-
58
- return {
59
- handleScroll
60
- }
61
- }