@wyxos/vibe 1.3.1 → 1.4.1

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.
@@ -0,0 +1,177 @@
1
+ import type { LayoutOptions, MasonryItem, ProcessedMasonryItem } from './types'
2
+
3
+ function getScrollbarWidth(): number {
4
+ const div = document.createElement('div')
5
+ div.style.visibility = 'hidden'
6
+ div.style.overflow = 'scroll'
7
+ ;(div.style as any).msOverflowStyle = 'scrollbar'
8
+ div.style.width = '100px'
9
+ div.style.height = '100px'
10
+ document.body.appendChild(div)
11
+
12
+ const inner = document.createElement('div')
13
+ inner.style.width = '100%'
14
+ div.appendChild(inner)
15
+
16
+ const scrollbarWidth = div.offsetWidth - inner.offsetWidth
17
+ document.body.removeChild(div)
18
+ return scrollbarWidth
19
+ }
20
+
21
+ export default function calculateLayout(
22
+ items: MasonryItem[],
23
+ container: HTMLElement,
24
+ columnCount: number,
25
+ options: LayoutOptions = {}
26
+ ): ProcessedMasonryItem[] {
27
+ const {
28
+ gutterX = 0,
29
+ gutterY = 0,
30
+ header = 0,
31
+ footer = 0,
32
+ paddingLeft = 0,
33
+ paddingRight = 0,
34
+ sizes = {
35
+ base: 1,
36
+ sm: 2,
37
+ md: 3,
38
+ lg: 4,
39
+ xl: 5,
40
+ '2xl': 6
41
+ },
42
+ placement = 'masonry'
43
+ } = options
44
+
45
+ let cssPaddingLeft = 0
46
+ let cssPaddingRight = 0
47
+ try {
48
+ if (container && container.nodeType === 1 && typeof window !== 'undefined' && window.getComputedStyle) {
49
+ const styles = window.getComputedStyle(container)
50
+ cssPaddingLeft = parseFloat(styles.paddingLeft) || 0
51
+ cssPaddingRight = parseFloat(styles.paddingRight) || 0
52
+ }
53
+ } catch {
54
+ // noop
55
+ }
56
+
57
+ const effectivePaddingLeft = (paddingLeft || 0) + cssPaddingLeft
58
+ const effectivePaddingRight = (paddingRight || 0) + cssPaddingRight
59
+
60
+ const measuredScrollbarWidth = container.offsetWidth - container.clientWidth
61
+ const scrollbarWidth = measuredScrollbarWidth > 0 ? measuredScrollbarWidth + 2 : getScrollbarWidth() + 2
62
+
63
+ const usableWidth = container.offsetWidth - scrollbarWidth - effectivePaddingLeft - effectivePaddingRight
64
+ const totalGutterX = gutterX * (columnCount - 1)
65
+ const columnWidth = Math.floor((usableWidth - totalGutterX) / columnCount)
66
+
67
+ const baseHeights = items.map((item) => {
68
+ const originalWidth = item.width
69
+ const originalHeight = item.height
70
+ const imageHeight = Math.round((columnWidth * originalHeight) / originalWidth)
71
+ return imageHeight + footer + header
72
+ })
73
+
74
+ if (placement === 'sequential-balanced') {
75
+ const n = baseHeights.length
76
+ if (n === 0) return []
77
+
78
+ const addWithGutter = (currentSum: number, itemsInGroup: number, nextHeight: number) => {
79
+ return currentSum + (itemsInGroup > 0 ? gutterY : 0) + nextHeight
80
+ }
81
+
82
+ let low = Math.max(...baseHeights)
83
+ let high = baseHeights.reduce((sum, h) => sum + h, 0) + gutterY * Math.max(0, n - 1)
84
+
85
+ const canPartition = (cap: number) => {
86
+ let groups = 1
87
+ let sum = 0
88
+ let count = 0
89
+ for (let i = 0; i < n; i++) {
90
+ const h = baseHeights[i]
91
+ const next = addWithGutter(sum, count, h)
92
+ if (next <= cap) {
93
+ sum = next
94
+ count++
95
+ } else {
96
+ groups++
97
+ sum = h
98
+ count = 1
99
+ if (h > cap) return false
100
+ if (groups > columnCount) return false
101
+ }
102
+ }
103
+ return groups <= columnCount
104
+ }
105
+
106
+ while (low < high) {
107
+ const mid = Math.floor((low + high) / 2)
108
+ if (canPartition(mid)) high = mid
109
+ else low = mid + 1
110
+ }
111
+ const cap = high
112
+
113
+ const starts = new Array<number>(columnCount).fill(0)
114
+ let groupIndex = columnCount - 1
115
+ let sum = 0
116
+ let count = 0
117
+ for (let i = n - 1; i >= 0; i--) {
118
+ const h = baseHeights[i]
119
+ const needAtLeast = i < groupIndex
120
+ const canFit = addWithGutter(sum, count, h) <= cap
121
+ if (!canFit || needAtLeast) {
122
+ starts[groupIndex] = i + 1
123
+ groupIndex--
124
+ sum = h
125
+ count = 1
126
+ } else {
127
+ sum = addWithGutter(sum, count, h)
128
+ count++
129
+ }
130
+ }
131
+ starts[0] = 0
132
+
133
+ const processedItems: ProcessedMasonryItem[] = []
134
+ const tops = new Array<number>(columnCount).fill(0)
135
+ for (let col = 0; col < columnCount; col++) {
136
+ const start = starts[col]
137
+ const end = col + 1 < columnCount ? starts[col + 1] : n
138
+ const left = col * (columnWidth + gutterX)
139
+ for (let i = start; i < end; i++) {
140
+ const item = items[i]
141
+ const newItem: ProcessedMasonryItem = { ...(item as any), columnWidth, imageHeight: 0, columnHeight: 0, left: 0, top: 0 }
142
+ const imageHeight = baseHeights[i] - (footer + header)
143
+ newItem.imageHeight = imageHeight
144
+ newItem.columnHeight = baseHeights[i]
145
+ newItem.left = left
146
+ newItem.top = tops[col]
147
+ tops[col] += newItem.columnHeight + (i + 1 < end ? gutterY : 0)
148
+ processedItems.push(newItem)
149
+ }
150
+ }
151
+ return processedItems
152
+ }
153
+
154
+ const columnHeights = new Array<number>(columnCount).fill(0)
155
+ const processedItems: ProcessedMasonryItem[] = []
156
+
157
+ for (let index = 0; index < items.length; index++) {
158
+ const item = items[index]
159
+ const newItem: ProcessedMasonryItem = { ...(item as any), columnWidth: 0, imageHeight: 0, columnHeight: 0, left: 0, top: 0 }
160
+
161
+ const col = columnHeights.indexOf(Math.min(...columnHeights))
162
+ const originalWidth = item.width
163
+ const originalHeight = item.height
164
+
165
+ newItem.columnWidth = columnWidth
166
+ newItem.left = col * (columnWidth + gutterX)
167
+ newItem.imageHeight = Math.round((columnWidth * originalHeight) / originalWidth)
168
+ newItem.columnHeight = newItem.imageHeight + footer + header
169
+ newItem.top = columnHeights[col]
170
+
171
+ columnHeights[col] += newItem.columnHeight + gutterY
172
+
173
+ processedItems.push(newItem)
174
+ }
175
+
176
+ return processedItems
177
+ }
@@ -1,5 +1,5 @@
1
- import { createApp } from 'vue'
2
- import './style.css'
3
- import App from './App.vue'
4
-
5
- createApp(App).mount('#app')
1
+ import { createApp } from 'vue'
2
+ import './style.css'
3
+ import App from './App.vue'
4
+
5
+ createApp(App).mount('#app')
@@ -1,7 +1,9 @@
1
+ import type { LayoutOptions, ProcessedMasonryItem } from './types'
2
+
1
3
  /**
2
4
  * Get responsive column count based on window width and layout sizes
3
5
  */
4
- export function getColumnCount(layout) {
6
+ export function getColumnCount(layout: Pick<LayoutOptions, 'sizes'> & { sizes: Required<NonNullable<LayoutOptions['sizes']>> }): number {
5
7
  const width = window.innerWidth
6
8
  const sizes = layout.sizes
7
9
 
@@ -16,23 +18,19 @@ export function getColumnCount(layout) {
16
18
  /**
17
19
  * Calculate container height based on item positions
18
20
  */
19
- export function calculateContainerHeight(items) {
21
+ export function calculateContainerHeight(items: ProcessedMasonryItem[]): number {
20
22
  const contentHeight = items.reduce((acc, item) => {
21
23
  return Math.max(acc, item.top + item.columnHeight)
22
24
  }, 0)
23
-
24
- // Add 500px buffer to the container height
25
25
  return contentHeight + 500
26
26
  }
27
27
 
28
28
  /**
29
29
  * Get style object for masonry item positioning
30
30
  */
31
- export function getItemStyle(item) {
31
+ export function getItemStyle(item: ProcessedMasonryItem): Record<string, string> {
32
32
  return {
33
- // Use transform-based positioning for smooth, compositor-driven movement
34
- transform: `translate3d(${item.left}px, ${item.top}px, 0)`,
35
- // Keep top/left at 0 so only transform changes between layouts
33
+ transform: `translate3d(${item.left}px, ${item.top}px, 0)` ,
36
34
  top: '0px',
37
35
  left: '0px',
38
36
  width: `${item.columnWidth}px`,
@@ -43,12 +41,12 @@ export function getItemStyle(item) {
43
41
  /**
44
42
  * Get item attributes for rendering
45
43
  */
46
- export function getItemAttributes(item, index = 0) {
44
+ export function getItemAttributes(item: ProcessedMasonryItem, index: number = 0): Record<string, any> {
47
45
  return {
48
46
  style: getItemStyle(item),
49
47
  'data-top': item.top,
50
48
  'data-left': item.left,
51
- 'data-id': `${item.page}-${item.id}`,
49
+ 'data-id': `${(item as any).page}-${(item as any).id}`,
52
50
  'data-index': index,
53
51
  }
54
52
  }
@@ -56,8 +54,8 @@ export function getItemAttributes(item, index = 0) {
56
54
  /**
57
55
  * Calculate column heights for masonry layout
58
56
  */
59
- export function calculateColumnHeights(items, columnCount) {
60
- const heights = new Array(columnCount).fill(0)
57
+ export function calculateColumnHeights(items: ProcessedMasonryItem[], columnCount: number): number[] {
58
+ const heights = new Array<number>(columnCount).fill(0)
61
59
  for (let i = 0; i < items.length; i++) {
62
60
  const item = items[i]
63
61
  const col = i % columnCount
package/src/types.ts ADDED
@@ -0,0 +1,38 @@
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 }
@@ -1,5 +1,6 @@
1
- import { nextTick } from 'vue'
2
- import { calculateColumnHeights } from './masonryUtils.js'
1
+ import { nextTick, type Ref } from 'vue'
2
+ import { calculateColumnHeights } from './masonryUtils'
3
+ import type { ProcessedMasonryItem } from './types'
3
4
 
4
5
  /**
5
6
  * Composable for handling masonry scroll behavior and item cleanup
@@ -14,107 +15,100 @@ export function useMasonryScroll({
14
15
  pageSize,
15
16
  refreshLayout,
16
17
  setItemsRaw,
17
- loadNext
18
+ loadNext,
19
+ leaveEstimateMs
20
+ }: {
21
+ container: Ref<HTMLElement | null>
22
+ masonry: Ref<ProcessedMasonryItem[]>
23
+ columns: Ref<number>
24
+ containerHeight: Ref<number>
25
+ isLoading: Ref<boolean>
26
+ maxItems: number
27
+ pageSize: number
28
+ refreshLayout: (items: ProcessedMasonryItem[]) => void
29
+ setItemsRaw: (items: ProcessedMasonryItem[]) => void
30
+ loadNext: () => Promise<any>
31
+ leaveEstimateMs?: number
18
32
  }) {
19
33
  let cleanupInProgress = false
34
+ let lastScrollTop = 0
20
35
 
21
36
  async function handleScroll() {
37
+ if (!container.value) return
38
+
22
39
  const { scrollTop, clientHeight } = container.value
23
40
  const visibleBottom = scrollTop + clientHeight
24
41
 
42
+ // Determine scroll direction (down only)
43
+ const isScrollingDown = scrollTop > lastScrollTop + 1 // tolerate tiny jitter
44
+ lastScrollTop = scrollTop
45
+
25
46
  const columnHeights = calculateColumnHeights(masonry.value, columns.value)
26
- // Use the longest column instead of shortest for better trigger timing
27
47
  const longestColumn = Math.max(...columnHeights)
28
48
  const whitespaceVisible = longestColumn + 300 < visibleBottom - 1
29
49
  const reachedContainerBottom = scrollTop + clientHeight >= containerHeight.value - 1
30
50
 
31
- if ((whitespaceVisible || reachedContainerBottom) && !isLoading.value && !cleanupInProgress) {
51
+ if ((whitespaceVisible || reachedContainerBottom) && isScrollingDown && !isLoading.value && !cleanupInProgress) {
32
52
  try {
33
- // Handle cleanup when too many items
34
53
  if (masonry.value.length > maxItems) {
35
54
  await handleItemCleanup(columnHeights)
36
55
  }
37
56
 
38
- await loadNext() // loadNext manages its own loading state and error handling
57
+ await loadNext()
39
58
  await nextTick()
40
59
  } catch (error) {
41
60
  console.error('Error in scroll handler:', error)
42
- // loadNext already handles its own loading state, no need to reset here
43
61
  }
44
62
  }
45
63
  }
46
64
 
47
- async function handleItemCleanup(columnHeightsBefore) {
48
- if (!masonry.value.length) {
49
- return
50
- }
65
+ async function handleItemCleanup(columnHeightsBefore: number[]) {
66
+ if (!masonry.value.length) return
67
+ if (masonry.value.length <= pageSize) return
51
68
 
52
- if (masonry.value.length <= pageSize) {
53
- // If we have fewer items than pageSize, no cleanup needed
54
- return
55
- }
56
-
57
- // Group items by page to understand page structure
58
- const pageGroups = masonry.value.reduce((acc, item) => {
59
- if (!acc[item.page]) {
60
- acc[item.page] = []
61
- }
62
- acc[item.page].push(item)
69
+ const pageGroups: Record<string | number, ProcessedMasonryItem[]> = masonry.value.reduce((acc, item) => {
70
+ const key = (item as any).page
71
+ if (!acc[key]) acc[key] = []
72
+ acc[key].push(item)
63
73
  return acc
64
- }, {})
74
+ }, {} as Record<string | number, ProcessedMasonryItem[]>)
65
75
 
66
76
  const pages = Object.keys(pageGroups).sort((a, b) => parseInt(a) - parseInt(b))
67
-
68
- if (pages.length === 0) {
69
- return
70
- }
77
+ if (pages.length === 0) return
71
78
 
72
79
  let totalRemovedItems = 0
73
- let pagesToRemove = []
80
+ const pagesToRemove: string[] = []
74
81
 
75
- // Remove pages cumulatively until we reach at least pageSize items
76
82
  for (const page of pages) {
77
83
  pagesToRemove.push(page)
78
84
  totalRemovedItems += pageGroups[page].length
79
-
80
- if (totalRemovedItems >= pageSize) {
81
- break
82
- }
85
+ if (totalRemovedItems >= pageSize) break
83
86
  }
84
87
 
85
- // Phase 1: remove items (trigger leave) WITHOUT moving remaining items
86
- const remainingItems = masonry.value.filter(item => !pagesToRemove.includes(item.page.toString()))
87
-
88
- if (remainingItems.length === masonry.value.length) {
89
- // No items were removed, nothing to do
90
- return
91
- }
88
+ const remainingItems = masonry.value.filter(item => !pagesToRemove.includes(String((item as any).page)))
89
+ if (remainingItems.length === masonry.value.length) return
92
90
 
93
91
  cleanupInProgress = true
94
92
 
95
- // Set raw items so TransitionGroup triggers leave on removed items, but remaining items keep their current transforms
96
93
  setItemsRaw(remainingItems)
97
-
98
94
  await nextTick()
99
- // Wait for leave transitions to complete; use a conservative timeout
100
- await waitFor(msLeaveEstimate())
95
+ // Allow leave to start, then FLIP survivors concurrently (single RAF)
96
+ await new Promise<void>(r => requestAnimationFrame(() => r()))
101
97
 
102
- // Phase 2: now recompute layout and animate moves for remaining items
103
98
  refreshLayout(remainingItems)
104
99
  await nextTick()
105
100
 
106
- // Maintain anchor after moves are applied
107
101
  await maintainAnchorPosition()
108
102
 
109
103
  cleanupInProgress = false
110
104
  }
111
105
 
112
106
  function msLeaveEstimate() {
113
- // Default estimate in ms; tweak if you change CSS leave timings
114
- return 700
107
+ const base = typeof leaveEstimateMs === 'number' && leaveEstimateMs > 0 ? leaveEstimateMs : 250
108
+ return base + 50
115
109
  }
116
110
 
117
- function waitFor(ms) {
111
+ function waitFor(ms: number) {
118
112
  return new Promise(resolve => setTimeout(resolve, ms))
119
113
  }
120
114
 
@@ -122,17 +116,14 @@ export function useMasonryScroll({
122
116
  if (!container.value) return
123
117
 
124
118
  const { scrollTop, clientHeight } = container.value
125
- const pivotY = scrollTop + clientHeight * 0.4 // aim to keep ~40% down the viewport stable
119
+ const pivotY = scrollTop + clientHeight * 0.4
126
120
 
127
- // Recompute column heights with the new layout
128
121
  const heights = calculateColumnHeights(masonry.value, columns.value)
129
122
  const anchorColumnIndex = heights.indexOf(Math.max(...heights))
130
123
 
131
- // Find items belonging to the anchor column
132
124
  const itemsInAnchor = masonry.value.filter((_, index) => index % columns.value === anchorColumnIndex)
133
125
  if (itemsInAnchor.length === 0) return
134
126
 
135
- // Choose the item whose top is the largest <= pivotY (closest above pivot)
136
127
  let pivotItem = itemsInAnchor[0]
137
128
  for (const it of itemsInAnchor) {
138
129
  if (it.top <= pivotY && it.top >= pivotItem.top) {
@@ -142,13 +133,11 @@ export function useMasonryScroll({
142
133
 
143
134
  const desiredTop = Math.max(0, pivotItem.top - clientHeight * 0.4)
144
135
 
145
- // Only adjust if we drifted significantly (> 4px) to avoid tiny corrections
146
136
  if (Math.abs(desiredTop - scrollTop) > 4) {
147
137
  container.value.scrollTo({ top: desiredTop, behavior: 'auto' })
148
138
  }
149
139
  }
150
140
 
151
- // Legacy function kept for compatibility; prefer maintainAnchorPosition()
152
141
  async function adjustScrollPosition() {
153
142
  await maintainAnchorPosition()
154
143
  }
@@ -1,25 +1,25 @@
1
1
  /**
2
- * Composable for handling masonry item transitions
2
+ * Composable for handling masonry item transitions (typed)
3
3
  */
4
- export function useMasonryTransitions(masonry) {
5
- function onEnter(el, done) {
6
- // Animate to its final transform (translate3d(left, top, 0)) with subtle scale/opacity
4
+ export function useMasonryTransitions(masonry: any) {
5
+ function onEnter(el: HTMLElement, done: () => void) {
7
6
  const left = parseInt(el.dataset.left || '0', 10)
8
7
  const top = parseInt(el.dataset.top || '0', 10)
9
8
  const index = parseInt(el.dataset.index || '0', 10)
10
9
 
11
- // Small stagger per item, capped
12
10
  const delay = Math.min(index * 20, 160)
13
-
14
- // Apply delay only for the enter; avoid affecting move transitions
15
- const prevDelay = el.style.transitionDelay
16
- el.style.transitionDelay = `${delay}ms`
11
+ const prevOpacityDelay = el.style.getPropertyValue('--masonry-opacity-delay')
12
+ el.style.setProperty('--masonry-opacity-delay', `${delay}ms`)
17
13
 
18
14
  requestAnimationFrame(() => {
19
15
  el.style.opacity = '1'
20
16
  el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
21
17
  const clear = () => {
22
- el.style.transitionDelay = prevDelay || '' // restore
18
+ if (prevOpacityDelay) {
19
+ el.style.setProperty('--masonry-opacity-delay', prevOpacityDelay)
20
+ } else {
21
+ el.style.removeProperty('--masonry-opacity-delay')
22
+ }
23
23
  el.removeEventListener('transitionend', clear)
24
24
  done()
25
25
  }
@@ -27,35 +27,41 @@ export function useMasonryTransitions(masonry) {
27
27
  })
28
28
  }
29
29
 
30
- function onBeforeEnter(el) {
31
- // Start slightly below and slightly smaller, faded
30
+ function onBeforeEnter(el: HTMLElement) {
32
31
  const left = parseInt(el.dataset.left || '0', 10)
33
32
  const top = parseInt(el.dataset.top || '0', 10)
34
33
  el.style.opacity = '0'
35
34
  el.style.transform = `translate3d(${left}px, ${top + 10}px, 0) scale(0.985)`
36
35
  }
37
36
 
38
- function onBeforeLeave(el) {
39
- // Ensure it is at its current transform position before animating
37
+ function onBeforeLeave(el: HTMLElement) {
40
38
  const left = parseInt(el.dataset.left || '0', 10)
41
39
  const top = parseInt(el.dataset.top || '0', 10)
42
40
  el.style.transition = 'none'
43
41
  el.style.opacity = '1'
44
42
  el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
45
- void el.offsetWidth // force reflow to flush style
46
- el.style.transition = '' // allow transition to apply again
43
+ el.style.removeProperty('--masonry-opacity-delay')
44
+ void el.offsetWidth
45
+ el.style.transition = ''
47
46
  }
48
47
 
49
- function onLeave(el, done) {
48
+ function onLeave(el: HTMLElement, done: () => void) {
50
49
  const left = parseInt(el.dataset.left || '0', 10)
51
50
  const top = parseInt(el.dataset.top || '0', 10)
52
51
 
53
- // Run on next frame to ensure transition styles are applied
52
+ const cs = getComputedStyle(el)
53
+ const varVal = cs.getPropertyValue('--masonry-leave-duration') || ''
54
+ const parsed = parseFloat(varVal)
55
+ const leaveMs = Number.isFinite(parsed) && parsed > 0 ? parsed : 200
56
+
57
+ const prevDuration = el.style.transitionDuration
58
+
54
59
  const cleanup = () => {
55
- el.removeEventListener('transitionend', onEnd)
60
+ el.removeEventListener('transitionend', onEnd as any)
56
61
  clearTimeout(fallback)
62
+ el.style.transitionDuration = prevDuration || ''
57
63
  }
58
- const onEnd = (e) => {
64
+ const onEnd = (e?: Event) => {
59
65
  if (!e || e.target === el) {
60
66
  cleanup()
61
67
  done()
@@ -64,12 +70,13 @@ export function useMasonryTransitions(masonry) {
64
70
  const fallback = setTimeout(() => {
65
71
  cleanup()
66
72
  done()
67
- }, 800)
73
+ }, leaveMs + 100)
68
74
 
69
75
  requestAnimationFrame(() => {
76
+ el.style.transitionDuration = `${leaveMs}ms`
70
77
  el.style.opacity = '0'
71
78
  el.style.transform = `translate3d(${left}px, ${top + 10}px, 0) scale(0.985)`
72
- el.addEventListener('transitionend', onEnd)
79
+ el.addEventListener('transitionend', onEnd as any)
73
80
  })
74
81
  }
75
82
 
package/index.js DELETED
@@ -1,10 +0,0 @@
1
- import Masonry from './src/Masonry.vue';
2
-
3
- export default {
4
- install(app) {
5
- app.component('WyxosMasonry', Masonry);
6
- app.component('WMasonry', Masonry);
7
- }
8
- };
9
-
10
- export { Masonry };
@@ -1,11 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
-
5
- import { describe, it, expect } from 'vitest';
6
-
7
- describe('Example Test', () => {
8
- it('should pass a basic truthiness check', () => {
9
- expect(true).toBe(true)
10
- })
11
- })
@@ -1,74 +0,0 @@
1
- function getScrollbarWidth() {
2
- // Create a temporary div
3
- const div = document.createElement('div')
4
- div.style.visibility = 'hidden'
5
- div.style.overflow = 'scroll' // force scrollbar
6
- div.style.msOverflowStyle = 'scrollbar' // for IE
7
- div.style.width = '100px'
8
- div.style.height = '100px'
9
-
10
- // Append to body
11
- document.body.appendChild(div)
12
-
13
- // Create inner div and measure difference
14
- const inner = document.createElement('div')
15
- inner.style.width = '100%'
16
- div.appendChild(inner)
17
-
18
- const scrollbarWidth = div.offsetWidth - inner.offsetWidth
19
-
20
- // Clean up
21
- document.body.removeChild(div)
22
-
23
- return scrollbarWidth
24
- }
25
-
26
- export default function calculateLayout(items, container, columnCount, options = {}) {
27
- const {
28
- gutterX = 0,
29
- gutterY = 0,
30
- header = 0,
31
- footer = 0,
32
- paddingLeft = 0,
33
- paddingRight = 0,
34
- sizes = {
35
- base: 1, // mobile-first default
36
- sm: 2, // ≥ 640px
37
- md: 3, // ≥ 768px
38
- lg: 4, // ≥ 1024px
39
- xl: 5, // ≥ 1280px
40
- '2xl': 6 // ≥ 1536px
41
- }
42
- } = options;
43
-
44
- const measuredScrollbarWidth = container.offsetWidth - container.clientWidth;
45
- const scrollbarWidth = measuredScrollbarWidth > 0 ? measuredScrollbarWidth + 2 : getScrollbarWidth() + 2;
46
- const usableWidth = container.offsetWidth - scrollbarWidth - paddingLeft - paddingRight;
47
- const totalGutterX = gutterX * (columnCount - 1);
48
- const columnWidth = Math.floor((usableWidth - totalGutterX) / columnCount);
49
-
50
- const columnHeights = new Array(columnCount).fill(0);
51
- const processedItems = [];
52
-
53
- for (let index = 0; index < items.length; index++) {
54
- const item = items[index];
55
- const newItem = { ...item };
56
-
57
- // Find the column with the shortest height for proper masonry layout
58
- const col = columnHeights.indexOf(Math.min(...columnHeights));
59
- const originalWidth = item.width;
60
- const originalHeight = item.height;
61
-
62
- newItem.columnWidth = columnWidth;
63
- newItem.left = col * (columnWidth + gutterX);
64
- newItem.imageHeight = Math.round((columnWidth * originalHeight) / originalWidth);
65
- newItem.columnHeight = newItem.imageHeight + footer + header;
66
- newItem.top = columnHeights[col];
67
-
68
- columnHeights[col] += newItem.columnHeight + gutterY;
69
-
70
- processedItems.push(newItem);
71
- }
72
-
73
- return processedItems;
74
- }