@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,20 +0,0 @@
1
- import { createRouter, createWebHashHistory } from 'vue-router'
2
-
3
- const router = createRouter({
4
- history: createWebHashHistory(), // Hash mode works perfectly with GitHub Pages
5
- routes: [
6
- {
7
- path: '/',
8
- name: 'home',
9
- component: () => import('../views/Home.vue')
10
- },
11
- {
12
- path: '/examples',
13
- name: 'examples',
14
- component: () => import('../views/Examples.vue')
15
- }
16
- ]
17
- })
18
-
19
- export default router
20
-
package/src/style.css DELETED
@@ -1,32 +0,0 @@
1
- @import "tailwindcss";
2
-
3
-
4
- :root {
5
- font-family: 'Inter', sans-serif;
6
- -webkit-font-smoothing: antialiased;
7
- -moz-osx-font-smoothing: grayscale;
8
- }
9
-
10
- body {
11
- background-color: #f8fafc;
12
- color: #0f172a;
13
- }
14
-
15
- /* Custom Scrollbar */
16
- ::-webkit-scrollbar {
17
- width: 8px;
18
- height: 8px;
19
- }
20
-
21
- ::-webkit-scrollbar-track {
22
- background: transparent;
23
- }
24
-
25
- ::-webkit-scrollbar-thumb {
26
- background: #93c5fd;
27
- border-radius: 4px;
28
- }
29
-
30
- ::-webkit-scrollbar-thumb:hover {
31
- background: #60a5fa;
32
- }
package/src/types.ts DELETED
@@ -1,101 +0,0 @@
1
- export type MasonryItem = {
2
- id: string
3
- width: number
4
- height: number
5
- page: number
6
- index: number
7
- src: string
8
- // allow extra fields
9
- [key: string]: any
10
- }
11
-
12
- export type ProcessedMasonryItem = MasonryItem & {
13
- columnWidth: number
14
- imageHeight: number
15
- columnHeight: number
16
- left: number
17
- top: number
18
- }
19
-
20
- export type LayoutOptions = {
21
- gutterX?: number
22
- gutterY?: number
23
- header?: number
24
- footer?: number
25
- paddingLeft?: number
26
- paddingRight?: number
27
- sizes?: {
28
- base: number
29
- sm?: number
30
- md?: number
31
- lg?: number
32
- xl?: number
33
- '2xl'?: number
34
- }
35
- placement?: 'masonry' | 'sequential-balanced'
36
- }
37
-
38
- export type GetPageResult = { items: MasonryItem[]; nextPage: number | null }
39
-
40
- /**
41
- * Type for the Masonry component instance (what's exposed via defineExpose)
42
- * Use this type when accessing the component via template refs
43
- */
44
- export interface MasonryInstance {
45
- // Cancels any ongoing load operations (page loads, backfills, etc.)
46
- cancelLoad: () => void
47
- // Opaque caller context passed through to getPage(page, context)
48
- context: any
49
- // Container height (wrapper element) in pixels
50
- containerHeight: number
51
- // Container width (wrapper element) in pixels
52
- containerWidth: number
53
- // Current Tailwind breakpoint name (base, sm, md, lg, xl, 2xl) based on containerWidth
54
- currentBreakpoint: string
55
- // Current page number or cursor being displayed
56
- currentPage: number | string | null
57
- // Completely destroys the component, clearing all state and resetting to initial state
58
- destroy: () => void
59
- // Boolean indicating if the end of the list has been reached (no more pages to load)
60
- hasReachedEnd: boolean
61
- // Initializes the component with items, page, and next page cursor. Use this for manual init mode.
62
- initialize: (items: MasonryItem[], page: number | string, next: number | string | null) => Promise<void> | void
63
- // Boolean indicating if the component has been initialized (first content has loaded)
64
- isInitialized: boolean
65
- // Boolean indicating if a page load or backfill operation is currently in progress
66
- isLoading: boolean
67
- // Error object if the last load operation failed, null otherwise
68
- loadError: Error | null
69
- // Loads the next page of items asynchronously
70
- loadNext: () => Promise<void>
71
- // Loads a specific page number or cursor asynchronously
72
- loadPage: (page: number | string) => Promise<void>
73
- // Array tracking pagination history (pages/cursors that have been loaded)
74
- paginationHistory: Array<number | string | null>
75
- // Refreshes the current page by clearing items and reloading from the current page
76
- refreshCurrentPage: () => Promise<void>
77
- // Recalculates the layout positions for all items. Call this after manually modifying items.
78
- refreshLayout: (items: MasonryItem[]) => void
79
- // Removes a single item from the masonry
80
- remove: (item: MasonryItem) => void
81
- // Removes all items from the masonry
82
- removeAll: () => void
83
- // Removes multiple items from the masonry in a single operation
84
- removeMany: (items: MasonryItem[]) => Promise<void> | void
85
- // Resets the component to initial state (clears items, resets pagination, scrolls to top)
86
- reset: () => void
87
- // Restores a single item at its original index (useful for undo operations)
88
- restore: (item: MasonryItem, index: number) => Promise<void> | void
89
- // Restores multiple items at their original indices (useful for undo operations)
90
- restoreMany: (items: MasonryItem[], indices: number[]) => Promise<void> | void
91
- // Scrolls the container to a specific position
92
- scrollTo: (position: number) => void
93
- // Scrolls the container to the top
94
- scrollToTop: () => void
95
- // Sets the opaque caller context (alternative to v-model:context)
96
- setContext: (val: any) => void
97
- // Sets fixed dimensions for the container, overriding ResizeObserver. Pass null to restore automatic sizing.
98
- setFixedDimensions: (dimensions: { width: number; height: number } | null) => void
99
- // Computed property returning the total number of items currently in the masonry
100
- totalItems: number
101
- }
@@ -1,59 +0,0 @@
1
- import { ref, type Ref } from 'vue'
2
-
3
- export interface UseMasonryDimensionsOptions {
4
- masonry: Ref<any[]>
5
- }
6
-
7
- export function useMasonryDimensions(options: UseMasonryDimensionsOptions) {
8
- const { masonry } = options
9
-
10
- // Track items with invalid dimensions to avoid duplicate warnings
11
- const invalidDimensionIds = ref<Set<number | string>>(new Set())
12
-
13
- function isPositiveNumber(value: any): boolean {
14
- return typeof value === 'number' && value > 0 && Number.isFinite(value)
15
- }
16
-
17
- function checkItemDimensions(items: any[], context: string) {
18
- try {
19
- if (!Array.isArray(items) || items.length === 0) return
20
- const missing = items.filter((item) => !isPositiveNumber(item?.width) || !isPositiveNumber(item?.height))
21
- if (missing.length === 0) return
22
-
23
- const newIds: Array<number | string> = []
24
- for (const item of missing) {
25
- const id = (item?.id as number | string | undefined) ?? `idx:${masonry.value.indexOf(item)}`
26
- if (!invalidDimensionIds.value.has(id)) {
27
- invalidDimensionIds.value.add(id)
28
- newIds.push(id)
29
- }
30
- }
31
- if (newIds.length > 0) {
32
- const sample = newIds.slice(0, 10)
33
- // eslint-disable-next-line no-console
34
- console.warn(
35
- '[Masonry] Items missing width/height detected:',
36
- {
37
- context,
38
- count: newIds.length,
39
- sampleIds: sample,
40
- hint: 'Ensure each item has positive width and height. Consider providing fallbacks (e.g., 512x512) at the data layer.'
41
- }
42
- )
43
- }
44
- } catch {
45
- // best-effort diagnostics only
46
- }
47
- }
48
-
49
- function reset() {
50
- invalidDimensionIds.value.clear()
51
- }
52
-
53
- return {
54
- checkItemDimensions,
55
- invalidDimensionIds,
56
- reset
57
- }
58
- }
59
-
@@ -1,231 +0,0 @@
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
-
@@ -1,164 +0,0 @@
1
- import { ref, nextTick, type Ref, type ComputedRef } from 'vue'
2
- import calculateLayout from './calculateLayout'
3
- import { getColumnCount, calculateContainerHeight } from './masonryUtils'
4
-
5
- export interface UseMasonryLayoutOptions {
6
- masonry: Ref<any[]>
7
- useSwipeMode: ComputedRef<boolean>
8
- container: Ref<HTMLElement | null>
9
- columns: Ref<number>
10
- containerWidth: Ref<number>
11
- masonryContentHeight: Ref<number>
12
- layout: ComputedRef<any>
13
- fixedDimensions: Ref<{ width?: number; height?: number } | null>
14
- checkItemDimensions: (items: any[], context: string) => void
15
- }
16
-
17
- export function useMasonryLayout(options: UseMasonryLayoutOptions) {
18
- const {
19
- masonry,
20
- useSwipeMode,
21
- container,
22
- columns,
23
- containerWidth,
24
- masonryContentHeight,
25
- layout,
26
- fixedDimensions,
27
- checkItemDimensions
28
- } = options
29
-
30
- // Cache previous layout state for incremental updates
31
- let previousLayoutItems: any[] = []
32
-
33
- function calculateHeight(content: any[]) {
34
- const newHeight = calculateContainerHeight(content as any)
35
- let floor = 0
36
- if (container.value) {
37
- const { scrollTop, clientHeight } = container.value
38
- floor = scrollTop + clientHeight + 100
39
- }
40
- masonryContentHeight.value = Math.max(newHeight, floor)
41
- }
42
-
43
- function refreshLayout(items: any[]) {
44
- if (useSwipeMode.value) {
45
- // In swipe mode, no layout calculation needed - items are stacked vertically
46
- masonry.value = items as any
47
- return
48
- }
49
-
50
- // Always update masonry value, even if container isn't ready
51
- // This ensures items are added in tests and when container isn't available yet
52
- masonry.value = items as any
53
-
54
- if (!container.value) return
55
- // Developer diagnostics: warn when dimensions are invalid
56
- checkItemDimensions(items as any[], 'refreshLayout')
57
-
58
- // Optimization: For large arrays, check if we can do incremental update
59
- // Only works if items were removed from the end (common case)
60
- const canUseIncremental = items.length > 1000 &&
61
- previousLayoutItems.length > items.length &&
62
- previousLayoutItems.length - items.length < 100 // Only small removals
63
-
64
- if (canUseIncremental) {
65
- // Check if items were removed from the end (most common case)
66
- let removedFromEnd = true
67
- for (let i = 0; i < items.length; i++) {
68
- if (items[i]?.id !== previousLayoutItems[i]?.id) {
69
- removedFromEnd = false
70
- break
71
- }
72
- }
73
-
74
- if (removedFromEnd) {
75
- // Items removed from end - we can reuse previous positions for remaining items
76
- // Just update indices and recalculate height
77
- const itemsWithIndex = items.map((item, index) => ({
78
- ...previousLayoutItems[index],
79
- originalIndex: index
80
- }))
81
-
82
- // Recalculate height only
83
- calculateHeight(itemsWithIndex as any)
84
- masonry.value = itemsWithIndex
85
- previousLayoutItems = itemsWithIndex
86
- return
87
- }
88
- }
89
-
90
- // Full recalculation (fallback for all other cases)
91
- // Update original index to reflect current position in array
92
- // This ensures indices are correct after items are removed
93
- const itemsWithIndex = items.map((item, index) => ({
94
- ...item,
95
- originalIndex: index
96
- }))
97
-
98
- // When fixed dimensions are set, ensure container uses the fixed width for layout
99
- // This prevents gaps when the container's actual width differs from the fixed width
100
- const containerEl = container.value as HTMLElement
101
- if (fixedDimensions.value && fixedDimensions.value.width !== undefined) {
102
- // Temporarily set width to match fixed dimensions for accurate layout calculation
103
- const originalWidth = containerEl.style.width
104
- const originalBoxSizing = containerEl.style.boxSizing
105
- containerEl.style.boxSizing = 'border-box'
106
- containerEl.style.width = `${fixedDimensions.value.width}px`
107
- // Force reflow
108
- containerEl.offsetWidth
109
-
110
- const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
111
-
112
- // Restore original width
113
- containerEl.style.width = originalWidth
114
- containerEl.style.boxSizing = originalBoxSizing
115
-
116
- calculateHeight(content as any)
117
- masonry.value = content
118
- // Cache for next incremental update
119
- previousLayoutItems = content
120
- } else {
121
- const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
122
- calculateHeight(content as any)
123
- masonry.value = content
124
- // Cache for next incremental update
125
- previousLayoutItems = content
126
- }
127
- }
128
-
129
- function setFixedDimensions(
130
- dimensions: { width?: number; height?: number } | null,
131
- updateScrollProgress?: () => void
132
- ) {
133
- fixedDimensions.value = dimensions
134
- if (dimensions) {
135
- if (dimensions.width !== undefined) containerWidth.value = dimensions.width
136
- // Force layout refresh when dimensions change
137
- if (!useSwipeMode.value && container.value && masonry.value.length > 0) {
138
- // Use nextTick to ensure DOM has updated
139
- nextTick(() => {
140
- columns.value = getColumnCount(layout.value as any, containerWidth.value)
141
- refreshLayout(masonry.value as any)
142
- if (updateScrollProgress) {
143
- updateScrollProgress()
144
- }
145
- })
146
- }
147
- }
148
- // When clearing fixed dimensions, restore from wrapper
149
- // Note: wrapper is not available in this composable, so this needs to be handled by caller
150
- }
151
-
152
- function onResize() {
153
- columns.value = getColumnCount(layout.value as any, containerWidth.value)
154
- refreshLayout(masonry.value as any)
155
- }
156
-
157
- return {
158
- refreshLayout,
159
- setFixedDimensions,
160
- onResize,
161
- calculateHeight
162
- }
163
- }
164
-