@wyxos/vibe 1.6.21 → 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.
@@ -392,7 +392,10 @@ watch(
392
392
  >
393
393
  <!-- Media type indicator badge - shown BEFORE preloading starts -->
394
394
  <div class="w-12 h-12 rounded-full bg-white/80 backdrop-blur-sm flex items-center justify-center shadow-sm">
395
- <i :class="mediaType === 'video' ? 'fas fa-video text-xl text-slate-400' : 'fas fa-image text-xl text-slate-400'"></i>
395
+ <!-- Allow custom icon via slot, fallback to default -->
396
+ <slot name="placeholder-icon" :mediaType="mediaType">
397
+ <i :class="mediaType === 'video' ? 'fas fa-video text-xl text-slate-400' : 'fas fa-image text-xl text-slate-400'"></i>
398
+ </slot>
396
399
  </div>
397
400
  </div>
398
401
 
@@ -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
+ }
@@ -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
+