@wyxos/vibe 1.6.20 → 1.6.22

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,60 +1,60 @@
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[]) {
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
- if (nearBottom && isScrollingDown && !isLoading.value) {
51
- await loadNext()
52
- await nextTick()
53
- return
54
- }
55
- }
56
-
57
- return {
58
- handleScroll
59
- }
60
- }
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[]) {
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
+ if (nearBottom && isScrollingDown && !isLoading.value) {
51
+ await loadNext()
52
+ await nextTick()
53
+ return
54
+ }
55
+ }
56
+
57
+ return {
58
+ handleScroll
59
+ }
60
+ }
@@ -1,11 +1,45 @@
1
1
  /**
2
2
  * Composable for handling masonry item transitions (typed)
3
+ * Optimized for large item arrays by skipping DOM operations for items outside viewport
3
4
  */
4
- export function useMasonryTransitions(masonry: any, opts?: { leaveDurationMs?: number }) {
5
+ export function useMasonryTransitions(refs: { container?: any; masonry?: any }, opts?: { leaveDurationMs?: number }) {
6
+ // Cache viewport bounds to avoid repeated calculations
7
+ let cachedViewportTop = 0
8
+ let cachedViewportBottom = 0
9
+ let cachedViewportHeight = 0
10
+ const VIEWPORT_BUFFER_PX = 1000 // Buffer zone for items near viewport
11
+
12
+ // Check if item is in viewport (with buffer) - optimized to skip DOM reads
13
+ function isItemInViewport(itemTop: number, itemHeight: number): boolean {
14
+ // Update cached viewport bounds if container exists
15
+ const container = refs.container?.value
16
+ if (container) {
17
+ const scrollTop = container.scrollTop
18
+ const clientHeight = container.clientHeight
19
+ cachedViewportTop = scrollTop - VIEWPORT_BUFFER_PX
20
+ cachedViewportBottom = scrollTop + clientHeight + VIEWPORT_BUFFER_PX
21
+ cachedViewportHeight = clientHeight
22
+ }
23
+
24
+ const itemBottom = itemTop + itemHeight
25
+ return itemBottom >= cachedViewportTop && itemTop <= cachedViewportBottom
26
+ }
27
+
5
28
  function onEnter(el: HTMLElement, done: () => void) {
6
29
  const left = parseInt(el.dataset.left || '0', 10)
7
30
  const top = parseInt(el.dataset.top || '0', 10)
8
31
  const index = parseInt(el.dataset.index || '0', 10)
32
+ // Get height from computed style or use a reasonable fallback
33
+ const height = el.offsetHeight || parseInt(getComputedStyle(el).height || '200', 10) || 200
34
+
35
+ // Skip animation for items outside viewport - just set position immediately
36
+ if (!isItemInViewport(top, height)) {
37
+ el.style.opacity = '1'
38
+ el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
39
+ el.style.transition = 'none'
40
+ done()
41
+ return
42
+ }
9
43
 
10
44
  const delay = Math.min(index * 20, 160)
11
45
  const prevOpacityDelay = el.style.getPropertyValue('--masonry-opacity-delay')
@@ -37,6 +71,14 @@ export function useMasonryTransitions(masonry: any, opts?: { leaveDurationMs?: n
37
71
  function onBeforeLeave(el: HTMLElement) {
38
72
  const left = parseInt(el.dataset.left || '0', 10)
39
73
  const top = parseInt(el.dataset.top || '0', 10)
74
+ const height = el.offsetHeight || parseInt(getComputedStyle(el).height || '200', 10) || 200
75
+
76
+ // Skip animation for items outside viewport
77
+ if (!isItemInViewport(top, height)) {
78
+ el.style.transition = 'none'
79
+ return
80
+ }
81
+
40
82
  el.style.transition = 'none'
41
83
  el.style.opacity = '1'
42
84
  el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
@@ -50,6 +92,15 @@ export function useMasonryTransitions(masonry: any, opts?: { leaveDurationMs?: n
50
92
  function onLeave(el: HTMLElement, done: () => void) {
51
93
  const left = parseInt(el.dataset.left || '0', 10)
52
94
  const top = parseInt(el.dataset.top || '0', 10)
95
+ const height = el.offsetHeight || parseInt(getComputedStyle(el).height || '200', 10) || 200
96
+
97
+ // Skip animation for items outside viewport - remove immediately
98
+ if (!isItemInViewport(top, height)) {
99
+ el.style.transition = 'none'
100
+ el.style.opacity = '0'
101
+ done()
102
+ return
103
+ }
53
104
 
54
105
  // Prefer explicit option, fallback to CSS variable for safety
55
106
  const fromOpts = typeof opts?.leaveDurationMs === 'number' ? opts!.leaveDurationMs : NaN
@@ -0,0 +1,233 @@
1
+ import { computed, ref, type Ref, type ComputedRef } from 'vue'
2
+
3
+ export interface UseSwipeModeOptions {
4
+ useSwipeMode: ComputedRef<boolean>
5
+ masonry: Ref<any[]>
6
+ isLoading: Ref<boolean>
7
+ loadNext: () => Promise<any>
8
+ loadPage: (page: any) => Promise<any>
9
+ paginationHistory: Ref<any[]>
10
+ }
11
+
12
+ export interface UseSwipeModeReturn {
13
+ // State
14
+ currentSwipeIndex: Ref<number>
15
+ swipeOffset: Ref<number>
16
+ isDragging: Ref<boolean>
17
+ swipeContainer: Ref<HTMLElement | null>
18
+
19
+ // Computed
20
+ currentItem: ComputedRef<any | null>
21
+ nextItem: ComputedRef<any | null>
22
+ previousItem: ComputedRef<any | null>
23
+
24
+ // Functions
25
+ handleTouchStart: (e: TouchEvent) => void
26
+ handleTouchMove: (e: TouchEvent) => void
27
+ handleTouchEnd: (e: TouchEvent) => void
28
+ handleMouseDown: (e: MouseEvent) => void
29
+ handleMouseMove: (e: MouseEvent) => void
30
+ handleMouseUp: (e: MouseEvent) => void
31
+ goToNextItem: () => void
32
+ goToPreviousItem: () => void
33
+ snapToCurrentItem: () => void
34
+ handleWindowResize: () => void
35
+ reset: () => void
36
+ }
37
+
38
+ export function useSwipeMode(options: UseSwipeModeOptions): UseSwipeModeReturn {
39
+ const { useSwipeMode, masonry, isLoading, loadNext, loadPage, paginationHistory } = options
40
+
41
+ // Swipe mode state
42
+ const currentSwipeIndex = ref<number>(0)
43
+ const swipeOffset = ref<number>(0)
44
+ const isDragging = ref<boolean>(false)
45
+ const dragStartY = ref<number>(0)
46
+ const dragStartOffset = ref<number>(0)
47
+ const swipeContainer = ref<HTMLElement | null>(null)
48
+
49
+ // Get current item index for swipe mode
50
+ const currentItem = computed(() => {
51
+ if (!useSwipeMode.value || masonry.value.length === 0) return null
52
+ const index = Math.max(0, Math.min(currentSwipeIndex.value, masonry.value.length - 1))
53
+ return masonry.value[index] || null
54
+ })
55
+
56
+ // Get next/previous items for preloading in swipe mode
57
+ const nextItem = computed(() => {
58
+ if (!useSwipeMode.value || !currentItem.value) return null
59
+ const nextIndex = currentSwipeIndex.value + 1
60
+ if (nextIndex >= masonry.value.length) return null
61
+ return masonry.value[nextIndex] || null
62
+ })
63
+
64
+ const previousItem = computed(() => {
65
+ if (!useSwipeMode.value || !currentItem.value) return null
66
+ const prevIndex = currentSwipeIndex.value - 1
67
+ if (prevIndex < 0) return null
68
+ return masonry.value[prevIndex] || null
69
+ })
70
+
71
+ function snapToCurrentItem() {
72
+ if (!swipeContainer.value) return
73
+
74
+ // Use container height for swipe mode instead of window height
75
+ const viewportHeight = swipeContainer.value.clientHeight
76
+ swipeOffset.value = -currentSwipeIndex.value * viewportHeight
77
+ }
78
+
79
+ function goToNextItem() {
80
+ if (!nextItem.value) {
81
+ // Try to load next page
82
+ loadNext()
83
+ return
84
+ }
85
+
86
+ currentSwipeIndex.value++
87
+ snapToCurrentItem()
88
+
89
+ // Preload next item if we're near the end
90
+ if (currentSwipeIndex.value >= masonry.value.length - 5) {
91
+ loadNext()
92
+ }
93
+ }
94
+
95
+ function goToPreviousItem() {
96
+ if (!previousItem.value) return
97
+
98
+ currentSwipeIndex.value--
99
+ snapToCurrentItem()
100
+ }
101
+
102
+ // Swipe gesture handlers
103
+ function handleTouchStart(e: TouchEvent) {
104
+ if (!useSwipeMode.value) return
105
+ isDragging.value = true
106
+ dragStartY.value = e.touches[0].clientY
107
+ dragStartOffset.value = swipeOffset.value
108
+ e.preventDefault()
109
+ }
110
+
111
+ function handleTouchMove(e: TouchEvent) {
112
+ if (!useSwipeMode.value || !isDragging.value) return
113
+ const deltaY = e.touches[0].clientY - dragStartY.value
114
+ swipeOffset.value = dragStartOffset.value + deltaY
115
+ e.preventDefault()
116
+ }
117
+
118
+ function handleTouchEnd(e: TouchEvent) {
119
+ if (!useSwipeMode.value || !isDragging.value) return
120
+ isDragging.value = false
121
+
122
+ const deltaY = swipeOffset.value - dragStartOffset.value
123
+ const threshold = 100 // Minimum swipe distance to trigger navigation
124
+
125
+ if (Math.abs(deltaY) > threshold) {
126
+ if (deltaY > 0 && previousItem.value) {
127
+ // Swipe down - go to previous
128
+ goToPreviousItem()
129
+ } else if (deltaY < 0 && nextItem.value) {
130
+ // Swipe up - go to next
131
+ goToNextItem()
132
+ } else {
133
+ // Snap back
134
+ snapToCurrentItem()
135
+ }
136
+ } else {
137
+ // Snap back if swipe wasn't far enough
138
+ snapToCurrentItem()
139
+ }
140
+
141
+ e.preventDefault()
142
+ }
143
+
144
+ // Mouse drag handlers for desktop testing
145
+ function handleMouseDown(e: MouseEvent) {
146
+ if (!useSwipeMode.value) return
147
+ isDragging.value = true
148
+ dragStartY.value = e.clientY
149
+ dragStartOffset.value = swipeOffset.value
150
+ e.preventDefault()
151
+ }
152
+
153
+ function handleMouseMove(e: MouseEvent) {
154
+ if (!useSwipeMode.value || !isDragging.value) return
155
+ const deltaY = e.clientY - dragStartY.value
156
+ swipeOffset.value = dragStartOffset.value + deltaY
157
+ e.preventDefault()
158
+ }
159
+
160
+ function handleMouseUp(e: MouseEvent) {
161
+ if (!useSwipeMode.value || !isDragging.value) return
162
+ isDragging.value = false
163
+
164
+ const deltaY = swipeOffset.value - dragStartOffset.value
165
+ const threshold = 100
166
+
167
+ if (Math.abs(deltaY) > threshold) {
168
+ if (deltaY > 0 && previousItem.value) {
169
+ goToPreviousItem()
170
+ } else if (deltaY < 0 && nextItem.value) {
171
+ goToNextItem()
172
+ } else {
173
+ snapToCurrentItem()
174
+ }
175
+ } else {
176
+ snapToCurrentItem()
177
+ }
178
+
179
+ e.preventDefault()
180
+ }
181
+
182
+ // Watch for container/window resize to update swipe mode
183
+ function handleWindowResize() {
184
+ // If switching from swipe to masonry, reset swipe state
185
+ if (!useSwipeMode.value && currentSwipeIndex.value > 0) {
186
+ currentSwipeIndex.value = 0
187
+ swipeOffset.value = 0
188
+ }
189
+
190
+ // If switching to swipe mode, ensure we have items loaded
191
+ if (useSwipeMode.value && masonry.value.length === 0 && !isLoading.value) {
192
+ loadPage(paginationHistory.value[0] as any)
193
+ }
194
+
195
+ // Re-snap to current item on resize to adjust offset
196
+ if (useSwipeMode.value) {
197
+ snapToCurrentItem()
198
+ }
199
+ }
200
+
201
+ function reset() {
202
+ currentSwipeIndex.value = 0
203
+ swipeOffset.value = 0
204
+ isDragging.value = false
205
+ }
206
+
207
+ return {
208
+ // State
209
+ currentSwipeIndex,
210
+ swipeOffset,
211
+ isDragging,
212
+ swipeContainer,
213
+
214
+ // Computed
215
+ currentItem,
216
+ nextItem,
217
+ previousItem,
218
+
219
+ // Functions
220
+ handleTouchStart,
221
+ handleTouchMove,
222
+ handleTouchEnd,
223
+ handleMouseDown,
224
+ handleMouseMove,
225
+ handleMouseUp,
226
+ goToNextItem,
227
+ goToPreviousItem,
228
+ snapToCurrentItem,
229
+ handleWindowResize,
230
+ reset
231
+ }
232
+ }
233
+
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Utility function to normalize errors to Error instances
3
+ * Ensures consistent error handling throughout the application
4
+ */
5
+ export function normalizeError(error: unknown): Error {
6
+ return error instanceof Error ? error : new Error(String(error))
7
+ }
8
+