@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.
- package/lib/index.js +1063 -1023
- package/lib/vibe.css +1 -1
- package/package.json +1 -1
- package/src/Masonry.vue +1030 -1008
- package/src/components/MasonryItem.vue +499 -501
- package/src/createMasonryTransitions.ts +18 -27
- package/src/types.ts +101 -38
- package/src/useMasonryItems.ts +231 -234
- package/src/useMasonryLayout.ts +164 -164
- package/src/useMasonryPagination.ts +116 -42
- package/src/useMasonryVirtualization.ts +1 -1
- package/src/views/Home.vue +2 -2
package/src/useMasonryItems.ts
CHANGED
|
@@ -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
|
|
76
|
-
// Start FLIP on next
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* @param
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
// a restored
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
await
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
|