@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,234 +1,231 @@
1
- import { nextTick, type Ref, type ComputedRef } from 'vue'
2
-
3
- export interface UseMasonryItemsOptions {
4
- masonry: Ref<any[]>
5
- useSwipeMode: ComputedRef<boolean>
6
- refreshLayout: (items: any[]) => void
7
- refreshCurrentPage: () => Promise<any>
8
- loadNext: () => Promise<any>
9
- maybeBackfillToTarget: (baseline: number, force?: boolean) => Promise<void>
10
- paginationHistory: Ref<any[]>
11
- }
12
-
13
- export function useMasonryItems(options: UseMasonryItemsOptions) {
14
- const {
15
- masonry,
16
- useSwipeMode,
17
- refreshLayout,
18
- refreshCurrentPage,
19
- loadNext,
20
- maybeBackfillToTarget,
21
- paginationHistory
22
- } = options
23
-
24
- // Batch remove operations to prevent visual glitches from rapid successive calls
25
- let pendingRemoves = new Set<any>()
26
- let removeTimeoutId: ReturnType<typeof setTimeout> | null = null
27
- let isProcessingRemoves = false
28
-
29
- async function processPendingRemoves() {
30
- if (pendingRemoves.size === 0 || isProcessingRemoves) return
31
-
32
- isProcessingRemoves = true
33
- const itemsToRemove = Array.from(pendingRemoves)
34
- pendingRemoves.clear()
35
- removeTimeoutId = null
36
-
37
- // Use removeManyInternal for batched removal (bypass batching to avoid recursion)
38
- await removeManyInternal(itemsToRemove)
39
- isProcessingRemoves = false
40
- }
41
-
42
- async function remove(item: any) {
43
- // Add to pending removes
44
- pendingRemoves.add(item)
45
-
46
- // Clear existing timeout
47
- if (removeTimeoutId) {
48
- clearTimeout(removeTimeoutId)
49
- }
50
-
51
- // Batch removes within a short time window (16ms = ~1 frame at 60fps)
52
- removeTimeoutId = setTimeout(() => {
53
- processPendingRemoves()
54
- }, 16)
55
- }
56
-
57
- async function removeManyInternal(items: any[]) {
58
- if (!items || items.length === 0) return
59
- const ids = new Set(items.map(i => i.id))
60
- const next = masonry.value.filter(i => !ids.has(i.id))
61
- masonry.value = next
62
- await nextTick()
63
-
64
- // If all items were removed, load next page
65
- if (next.length === 0 && paginationHistory.value.length > 0) {
66
- try {
67
- await loadNext()
68
- // Force backfill from 0 to ensure viewport is filled
69
- await maybeBackfillToTarget(0, true)
70
- } catch { }
71
- return
72
- }
73
-
74
- // Commit DOM updates without forcing sync reflow
75
- await new Promise<void>(r => requestAnimationFrame(() => r()))
76
- // Start FLIP on next frame
77
- requestAnimationFrame(() => {
78
- refreshLayout(next)
79
- })
80
- }
81
-
82
- async function removeMany(items: any[]) {
83
- if (!items || items.length === 0) return
84
-
85
- // Add all items to pending removes for batching
86
- items.forEach(item => pendingRemoves.add(item))
87
-
88
- // Clear existing timeout
89
- if (removeTimeoutId) {
90
- clearTimeout(removeTimeoutId)
91
- }
92
-
93
- // Batch removes within a short time window (16ms = ~1 frame at 60fps)
94
- removeTimeoutId = setTimeout(() => {
95
- processPendingRemoves()
96
- }, 16)
97
- }
98
-
99
- /**
100
- * Restore a single item at its original index.
101
- * This is useful for undo operations where an item needs to be restored to its exact position.
102
- * Handles all index calculation and layout recalculation internally.
103
- * @param item - Item to restore
104
- * @param index - Original index of the item
105
- */
106
- async function restore(item: any, index: number) {
107
- if (!item) return
108
-
109
- const current = masonry.value
110
- const existingIndex = current.findIndex(i => i.id === item.id)
111
- if (existingIndex !== -1) return // Item already exists
112
-
113
- // Insert at the original index (clamped to valid range)
114
- const newItems = [...current]
115
- const targetIndex = Math.min(index, newItems.length)
116
- newItems.splice(targetIndex, 0, item)
117
-
118
- // Update the masonry array
119
- masonry.value = newItems
120
- await nextTick()
121
-
122
- // Trigger layout recalculation (same pattern as remove)
123
- if (!useSwipeMode.value) {
124
- // Commit DOM updates without forcing sync reflow
125
- await new Promise<void>(r => requestAnimationFrame(() => r()))
126
- // Start FLIP on next frame
127
- requestAnimationFrame(() => {
128
- refreshLayout(newItems)
129
- })
130
- }
131
- }
132
-
133
- /**
134
- * Restore multiple items at their original indices.
135
- * This is useful for undo operations where items need to be restored to their exact positions.
136
- * Handles all index calculation and layout recalculation internally.
137
- * @param items - Array of items to restore
138
- * @param indices - Array of original indices for each item (must match items array length)
139
- */
140
- async function restoreMany(items: any[], indices: number[]) {
141
- if (!items || items.length === 0) return
142
- if (!indices || indices.length !== items.length) {
143
- console.warn('[Masonry] restoreMany: items and indices arrays must have the same length')
144
- return
145
- }
146
-
147
- const current = masonry.value
148
- const existingIds = new Set(current.map(i => i.id))
149
-
150
- // Filter out items that already exist and pair with their indices
151
- const itemsToRestore: Array<{ item: any; index: number }> = []
152
- for (let i = 0; i < items.length; i++) {
153
- if (!existingIds.has(items[i]?.id)) {
154
- itemsToRestore.push({ item: items[i], index: indices[i] })
155
- }
156
- }
157
-
158
- if (itemsToRestore.length === 0) return
159
-
160
- // Build the final array by merging current items and restored items
161
- // Strategy: Build position by position - for each position, decide if it should be
162
- // a restored item (at its original index) or a current item (accounting for shifts)
163
-
164
- // Create a map of restored items by their original index for O(1) lookup
165
- const restoredByIndex = new Map<number, any>()
166
- for (const { item, index } of itemsToRestore) {
167
- restoredByIndex.set(index, item)
168
- }
169
-
170
- // Find the maximum position we need to consider
171
- const maxRestoredIndex = itemsToRestore.length > 0
172
- ? Math.max(...itemsToRestore.map(({ index }) => index))
173
- : -1
174
- const maxPosition = Math.max(current.length - 1, maxRestoredIndex)
175
-
176
- // Build the final array position by position
177
- // Key insight: Current array items are in "shifted" positions (missing the removed items).
178
- // When we restore items at their original positions, current items naturally shift back.
179
- // We can build the final array by iterating positions and using items sequentially.
180
- const newItems: any[] = []
181
- let currentArrayIndex = 0 // Track which current item we should use next
182
-
183
- // Iterate through all positions up to the maximum we need
184
- for (let position = 0; position <= maxPosition; position++) {
185
- // If there's a restored item that belongs at this position, use it
186
- if (restoredByIndex.has(position)) {
187
- newItems.push(restoredByIndex.get(position)!)
188
- } else {
189
- // Otherwise, this position should be filled by the next current item
190
- // Since current array is missing restored items, items are shifted left.
191
- // By using them sequentially, they naturally end up in the correct positions.
192
- if (currentArrayIndex < current.length) {
193
- newItems.push(current[currentArrayIndex])
194
- currentArrayIndex++
195
- }
196
- }
197
- }
198
-
199
- // Add any remaining current items that come after the last restored position
200
- // (These are items that were originally after maxRestoredIndex)
201
- while (currentArrayIndex < current.length) {
202
- newItems.push(current[currentArrayIndex])
203
- currentArrayIndex++
204
- }
205
-
206
- // Update the masonry array
207
- masonry.value = newItems
208
- await nextTick()
209
-
210
- // Trigger layout recalculation (same pattern as removeMany)
211
- if (!useSwipeMode.value) {
212
- // Commit DOM updates without forcing sync reflow
213
- await new Promise<void>(r => requestAnimationFrame(() => r()))
214
- // Start FLIP on next frame
215
- requestAnimationFrame(() => {
216
- refreshLayout(newItems)
217
- })
218
- }
219
- }
220
-
221
- async function removeAll() {
222
- // Clear all items
223
- masonry.value = []
224
- }
225
-
226
- return {
227
- remove,
228
- removeMany,
229
- restore,
230
- restoreMany,
231
- removeAll
232
- }
233
- }
234
-
1
+ import { nextTick, type Ref, type ComputedRef } from 'vue'
2
+
3
+ export interface UseMasonryItemsOptions {
4
+ masonry: Ref<any[]>
5
+ useSwipeMode: ComputedRef<boolean>
6
+ refreshLayout: (items: any[]) => void
7
+ refreshCurrentPage: () => Promise<any>
8
+ loadNext: () => Promise<any>
9
+ maybeBackfillToTarget: (baseline: number, force?: boolean) => Promise<void>
10
+ paginationHistory: Ref<any[]>
11
+ }
12
+
13
+ export function useMasonryItems(options: UseMasonryItemsOptions) {
14
+ const {
15
+ masonry,
16
+ useSwipeMode,
17
+ refreshLayout,
18
+ refreshCurrentPage,
19
+ loadNext,
20
+ maybeBackfillToTarget,
21
+ paginationHistory
22
+ } = options
23
+
24
+ // Batch remove operations to prevent visual glitches from rapid successive calls
25
+ let pendingRemoves = new Set<any>()
26
+ let removeTimeoutId: ReturnType<typeof setTimeout> | null = null
27
+ let isProcessingRemoves = false
28
+
29
+ async function processPendingRemoves() {
30
+ if (pendingRemoves.size === 0 || isProcessingRemoves) return
31
+
32
+ isProcessingRemoves = true
33
+ const itemsToRemove = Array.from(pendingRemoves)
34
+ pendingRemoves.clear()
35
+ removeTimeoutId = null
36
+
37
+ // Use removeManyInternal for batched removal (bypass batching to avoid recursion)
38
+ await removeManyInternal(itemsToRemove)
39
+ isProcessingRemoves = false
40
+ }
41
+
42
+ async function remove(item: any) {
43
+ // Add to pending removes
44
+ pendingRemoves.add(item)
45
+
46
+ // Clear existing timeout
47
+ if (removeTimeoutId) {
48
+ clearTimeout(removeTimeoutId)
49
+ }
50
+
51
+ // Batch removes within a short time window (16ms = ~1 frame at 60fps)
52
+ removeTimeoutId = setTimeout(() => {
53
+ processPendingRemoves()
54
+ }, 16)
55
+ }
56
+
57
+ async function removeManyInternal(items: any[]) {
58
+ if (!items || items.length === 0) return
59
+ const ids = new Set(items.map(i => i.id))
60
+ const next = masonry.value.filter(i => !ids.has(i.id))
61
+ masonry.value = next
62
+ await nextTick()
63
+
64
+ // If all items were removed, load next page
65
+ if (next.length === 0 && paginationHistory.value.length > 0) {
66
+ try {
67
+ await loadNext()
68
+ // Force backfill from 0 to ensure viewport is filled
69
+ await maybeBackfillToTarget(0, true)
70
+ } catch { }
71
+ return
72
+ }
73
+
74
+ // Commit DOM updates without forcing sync reflow
75
+ await nextTick()
76
+ // Start FLIP on next tick
77
+ await nextTick()
78
+ refreshLayout(next)
79
+ }
80
+
81
+ async function removeMany(items: any[]) {
82
+ if (!items || items.length === 0) return
83
+
84
+ // Add all items to pending removes for batching
85
+ items.forEach(item => pendingRemoves.add(item))
86
+
87
+ // Clear existing timeout
88
+ if (removeTimeoutId) {
89
+ clearTimeout(removeTimeoutId)
90
+ }
91
+
92
+ // Batch removes within a short time window (16ms = ~1 frame at 60fps)
93
+ removeTimeoutId = setTimeout(() => {
94
+ processPendingRemoves()
95
+ }, 16)
96
+ }
97
+
98
+ /**
99
+ * Restore a single item at its original index.
100
+ * This is useful for undo operations where an item needs to be restored to its exact position.
101
+ * Handles all index calculation and layout recalculation internally.
102
+ * @param item - Item to restore
103
+ * @param index - Original index of the item
104
+ */
105
+ async function restore(item: any, index: number) {
106
+ if (!item) return
107
+
108
+ const current = masonry.value
109
+ const existingIndex = current.findIndex(i => i.id === item.id)
110
+ if (existingIndex !== -1) return // Item already exists
111
+
112
+ // Insert at the original index (clamped to valid range)
113
+ const newItems = [...current]
114
+ const targetIndex = Math.min(index, newItems.length)
115
+ newItems.splice(targetIndex, 0, item)
116
+
117
+ // Update the masonry array
118
+ masonry.value = newItems
119
+ await nextTick()
120
+
121
+ // Trigger layout recalculation (same pattern as remove)
122
+ if (!useSwipeMode.value) {
123
+ // Commit DOM updates without forcing sync reflow
124
+ await nextTick()
125
+ // Start FLIP on next tick
126
+ await nextTick()
127
+ refreshLayout(newItems)
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Restore multiple items at their original indices.
133
+ * This is useful for undo operations where items need to be restored to their exact positions.
134
+ * Handles all index calculation and layout recalculation internally.
135
+ * @param items - Array of items to restore
136
+ * @param indices - Array of original indices for each item (must match items array length)
137
+ */
138
+ async function restoreMany(items: any[], indices: number[]) {
139
+ if (!items || items.length === 0) return
140
+ if (!indices || indices.length !== items.length) {
141
+ console.warn('[Masonry] restoreMany: items and indices arrays must have the same length')
142
+ return
143
+ }
144
+
145
+ const current = masonry.value
146
+ const existingIds = new Set(current.map(i => i.id))
147
+
148
+ // Filter out items that already exist and pair with their indices
149
+ const itemsToRestore: Array<{ item: any; index: number }> = []
150
+ for (let i = 0; i < items.length; i++) {
151
+ if (!existingIds.has(items[i]?.id)) {
152
+ itemsToRestore.push({ item: items[i], index: indices[i] })
153
+ }
154
+ }
155
+
156
+ if (itemsToRestore.length === 0) return
157
+
158
+ // Build the final array by merging current items and restored items
159
+ // Strategy: Build position by position - for each position, decide if it should be
160
+ // a restored item (at its original index) or a current item (accounting for shifts)
161
+
162
+ // Create a map of restored items by their original index for O(1) lookup
163
+ const restoredByIndex = new Map<number, any>()
164
+ for (const { item, index } of itemsToRestore) {
165
+ restoredByIndex.set(index, item)
166
+ }
167
+
168
+ // Find the maximum position we need to consider
169
+ const maxRestoredIndex = itemsToRestore.length > 0
170
+ ? Math.max(...itemsToRestore.map(({ index }) => index))
171
+ : -1
172
+ const maxPosition = Math.max(current.length - 1, maxRestoredIndex)
173
+
174
+ // Build the final array position by position
175
+ // Key insight: Current array items are in "shifted" positions (missing the removed items).
176
+ // When we restore items at their original positions, current items naturally shift back.
177
+ // We can build the final array by iterating positions and using items sequentially.
178
+ const newItems: any[] = []
179
+ let currentArrayIndex = 0 // Track which current item we should use next
180
+
181
+ // Iterate through all positions up to the maximum we need
182
+ for (let position = 0; position <= maxPosition; position++) {
183
+ // If there's a restored item that belongs at this position, use it
184
+ if (restoredByIndex.has(position)) {
185
+ newItems.push(restoredByIndex.get(position)!)
186
+ } else {
187
+ // Otherwise, this position should be filled by the next current item
188
+ // Since current array is missing restored items, items are shifted left.
189
+ // By using them sequentially, they naturally end up in the correct positions.
190
+ if (currentArrayIndex < current.length) {
191
+ newItems.push(current[currentArrayIndex])
192
+ currentArrayIndex++
193
+ }
194
+ }
195
+ }
196
+
197
+ // Add any remaining current items that come after the last restored position
198
+ // (These are items that were originally after maxRestoredIndex)
199
+ while (currentArrayIndex < current.length) {
200
+ newItems.push(current[currentArrayIndex])
201
+ currentArrayIndex++
202
+ }
203
+
204
+ // Update the masonry array
205
+ masonry.value = newItems
206
+ await nextTick()
207
+
208
+ // Trigger layout recalculation (same pattern as removeMany)
209
+ if (!useSwipeMode.value) {
210
+ // Commit DOM updates without forcing sync reflow
211
+ await nextTick()
212
+ // Start FLIP on next tick
213
+ await nextTick()
214
+ refreshLayout(newItems)
215
+ }
216
+ }
217
+
218
+ async function removeAll() {
219
+ // Clear all items
220
+ masonry.value = []
221
+ }
222
+
223
+ return {
224
+ remove,
225
+ removeMany,
226
+ restore,
227
+ restoreMany,
228
+ removeAll
229
+ }
230
+ }
231
+