@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.
- package/README.md +29 -287
- package/lib/index.cjs +1 -0
- package/lib/index.js +795 -1791
- package/lib/logo-dark.svg +36 -36
- package/lib/logo-light.svg +29 -29
- package/lib/logo.svg +32 -32
- package/lib/manifest.json +1 -1
- package/package.json +82 -96
- package/LICENSE +0 -21
- package/lib/vibe.css +0 -1
- package/lib/vite.svg +0 -1
- package/src/App.vue +0 -35
- package/src/Masonry.vue +0 -1030
- package/src/archive/App.vue +0 -96
- package/src/archive/InfiniteMansonry.spec.ts +0 -10
- package/src/archive/InfiniteMasonry.vue +0 -218
- package/src/assets/vue.svg +0 -1
- package/src/calculateLayout.ts +0 -194
- package/src/components/CodeTabs.vue +0 -158
- package/src/components/MasonryItem.vue +0 -499
- package/src/components/examples/BasicExample.vue +0 -46
- package/src/components/examples/CustomItemExample.vue +0 -87
- package/src/components/examples/HeaderFooterExample.vue +0 -79
- package/src/components/examples/ManualInitExample.vue +0 -78
- package/src/components/examples/SwipeModeExample.vue +0 -40
- package/src/createMasonryTransitions.ts +0 -176
- package/src/main.ts +0 -6
- package/src/masonryUtils.ts +0 -96
- package/src/pages.json +0 -36402
- package/src/router/index.ts +0 -20
- package/src/style.css +0 -32
- package/src/types.ts +0 -101
- package/src/useMasonryDimensions.ts +0 -59
- package/src/useMasonryItems.ts +0 -231
- package/src/useMasonryLayout.ts +0 -164
- package/src/useMasonryPagination.ts +0 -539
- package/src/useMasonryScroll.ts +0 -61
- package/src/useMasonryVirtualization.ts +0 -140
- package/src/useSwipeMode.ts +0 -233
- package/src/utils/errorHandler.ts +0 -8
- package/src/views/Examples.vue +0 -323
- package/src/views/Home.vue +0 -321
- package/toggle-link.mjs +0 -92
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Masonry
|
|
3
|
-
v-model:items="items"
|
|
4
|
-
:get-page="getPage"
|
|
5
|
-
:load-at-page="1"
|
|
6
|
-
:layout="layout"
|
|
7
|
-
init="auto"
|
|
8
|
-
>
|
|
9
|
-
<template #item-header="{ item }">
|
|
10
|
-
<div class="h-full flex items-center justify-between px-3">
|
|
11
|
-
<div class="flex items-center gap-2">
|
|
12
|
-
<div class="w-6 h-6 rounded-full bg-white/80 backdrop-blur-sm flex items-center justify-center shadow-sm">
|
|
13
|
-
<i :class="item.type === 'video' ? 'fas fa-video text-[10px] text-slate-500' : 'fas fa-image text-[10px] text-slate-500'"></i>
|
|
14
|
-
</div>
|
|
15
|
-
<span class="text-xs font-medium text-slate-700">#{{ String(item.id).split('-')[0] }}</span>
|
|
16
|
-
</div>
|
|
17
|
-
<span v-if="item.title" class="text-[11px] text-slate-600 truncate max-w-[160px]">{{ item.title }}</span>
|
|
18
|
-
</div>
|
|
19
|
-
</template>
|
|
20
|
-
|
|
21
|
-
<template #item-footer="{ item, remove }">
|
|
22
|
-
<div class="h-full flex items-center justify-between px-3">
|
|
23
|
-
<div class="flex items-center gap-2">
|
|
24
|
-
<button
|
|
25
|
-
v-if="remove"
|
|
26
|
-
class="px-2.5 py-1 rounded-full bg-white/90 text-slate-700 text-[11px] shadow-sm hover:bg-red-500 hover:text-white transition-colors"
|
|
27
|
-
@click.stop="remove(item)"
|
|
28
|
-
>
|
|
29
|
-
Remove
|
|
30
|
-
</button>
|
|
31
|
-
</div>
|
|
32
|
-
<div class="text-[11px] text-slate-600">
|
|
33
|
-
{{ item.width }}×{{ item.height }}
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</template>
|
|
37
|
-
</Masonry>
|
|
38
|
-
</template>
|
|
39
|
-
|
|
40
|
-
<script setup lang="ts">
|
|
41
|
-
import { ref } from 'vue'
|
|
42
|
-
import Masonry from '../../Masonry.vue'
|
|
43
|
-
import fixture from '../../pages.json'
|
|
44
|
-
import type { MasonryItem, GetPageResult } from '../../types'
|
|
45
|
-
|
|
46
|
-
const items = ref<MasonryItem[]>([])
|
|
47
|
-
|
|
48
|
-
const layout = {
|
|
49
|
-
sizes: { base: 1, sm: 2, md: 3, lg: 4 },
|
|
50
|
-
gutterX: 10,
|
|
51
|
-
gutterY: 10,
|
|
52
|
-
header: 36,
|
|
53
|
-
footer: 40
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const getPage = async (page: number): Promise<GetPageResult> => {
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
setTimeout(() => {
|
|
59
|
-
const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined
|
|
60
|
-
|
|
61
|
-
if (!pageData) {
|
|
62
|
-
resolve({
|
|
63
|
-
items: [],
|
|
64
|
-
nextPage: null
|
|
65
|
-
})
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
resolve({
|
|
70
|
-
items: pageData.items,
|
|
71
|
-
nextPage: page < (fixture as any[]).length ? page + 1 : null
|
|
72
|
-
})
|
|
73
|
-
}, 300)
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
</script>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="relative h-full">
|
|
3
|
-
<div
|
|
4
|
-
v-if="!isInitialized"
|
|
5
|
-
class="absolute inset-0 z-10 flex items-center justify-center bg-slate-900/40 backdrop-blur-sm"
|
|
6
|
-
>
|
|
7
|
-
<button
|
|
8
|
-
type="button"
|
|
9
|
-
class="inline-flex items-center gap-2 rounded-full bg-white px-5 py-2 text-sm font-semibold text-slate-900 shadow-sm ring-1 ring-slate-200 transition hover:-translate-y-0.5 hover:shadow-md"
|
|
10
|
-
@click="handleInit"
|
|
11
|
-
>
|
|
12
|
-
Load gallery
|
|
13
|
-
<span class="text-slate-400">-></span>
|
|
14
|
-
</button>
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
<Masonry
|
|
18
|
-
ref="masonryRef"
|
|
19
|
-
v-model:items="items"
|
|
20
|
-
:get-page="getPage"
|
|
21
|
-
:load-at-page="1"
|
|
22
|
-
:layout="layout"
|
|
23
|
-
init="manual"
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
26
|
-
</template>
|
|
27
|
-
|
|
28
|
-
<script setup lang="ts">
|
|
29
|
-
import { ref } from 'vue'
|
|
30
|
-
import Masonry from '../../Masonry.vue'
|
|
31
|
-
import fixture from '../../pages.json'
|
|
32
|
-
import type { MasonryItem, GetPageResult } from '../../types'
|
|
33
|
-
|
|
34
|
-
const items = ref<MasonryItem[]>([])
|
|
35
|
-
const isInitialized = ref(false)
|
|
36
|
-
const masonryRef = ref<InstanceType<typeof Masonry> | null>(null)
|
|
37
|
-
|
|
38
|
-
const layout = {
|
|
39
|
-
sizes: { base: 1, sm: 2, md: 3, lg: 4 },
|
|
40
|
-
gutterX: 10,
|
|
41
|
-
gutterY: 10
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const getPage = async (page: number): Promise<GetPageResult> => {
|
|
45
|
-
return new Promise((resolve) => {
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined
|
|
48
|
-
|
|
49
|
-
if (!pageData) {
|
|
50
|
-
resolve({
|
|
51
|
-
items: [],
|
|
52
|
-
nextPage: null
|
|
53
|
-
})
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
resolve({
|
|
58
|
-
items: pageData.items,
|
|
59
|
-
nextPage: page < (fixture as any[]).length ? page + 1 : null
|
|
60
|
-
})
|
|
61
|
-
}, 300)
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function handleInit() {
|
|
66
|
-
if (isInitialized.value) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
isInitialized.value = true
|
|
71
|
-
masonryRef.value?.loadPage(1)
|
|
72
|
-
}
|
|
73
|
-
</script>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Masonry
|
|
3
|
-
v-model:items="items"
|
|
4
|
-
:get-page="getPage"
|
|
5
|
-
:load-at-page="1"
|
|
6
|
-
layout-mode="swipe"
|
|
7
|
-
init="auto"
|
|
8
|
-
/>
|
|
9
|
-
</template>
|
|
10
|
-
|
|
11
|
-
<script setup lang="ts">
|
|
12
|
-
import { ref } from 'vue'
|
|
13
|
-
import Masonry from '../../Masonry.vue'
|
|
14
|
-
import fixture from '../../pages.json'
|
|
15
|
-
import type { MasonryItem, GetPageResult } from '../../types'
|
|
16
|
-
|
|
17
|
-
const items = ref<MasonryItem[]>([])
|
|
18
|
-
|
|
19
|
-
const getPage = async (page: number): Promise<GetPageResult> => {
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
setTimeout(() => {
|
|
22
|
-
const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined
|
|
23
|
-
|
|
24
|
-
if (!pageData) {
|
|
25
|
-
resolve({
|
|
26
|
-
items: [],
|
|
27
|
-
nextPage: null
|
|
28
|
-
})
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
resolve({
|
|
33
|
-
items: pageData.items,
|
|
34
|
-
nextPage: page < (fixture as any[]).length ? page + 1 : null
|
|
35
|
-
})
|
|
36
|
-
}, 300)
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
</script>
|
|
40
|
-
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Factory for masonry item transitions (typed)
|
|
3
|
-
* Optimized for large item arrays by skipping DOM operations for items outside viewport
|
|
4
|
-
*/
|
|
5
|
-
export function createMasonryTransitions(
|
|
6
|
-
refs: { container?: any; masonry?: any },
|
|
7
|
-
opts?: { leaveDurationMs?: number; virtualizing?: { value: boolean } }
|
|
8
|
-
) {
|
|
9
|
-
// Cache viewport bounds to avoid repeated calculations
|
|
10
|
-
let cachedViewportTop = 0
|
|
11
|
-
let cachedViewportBottom = 0
|
|
12
|
-
let cachedViewportHeight = 0
|
|
13
|
-
const VIEWPORT_BUFFER_PX = 1000 // Buffer zone for items near viewport
|
|
14
|
-
|
|
15
|
-
// Check if item is in viewport (with buffer) - optimized to skip DOM reads
|
|
16
|
-
function isItemInViewport(itemTop: number, itemHeight: number): boolean {
|
|
17
|
-
// Update cached viewport bounds if container exists
|
|
18
|
-
const container = refs.container?.value
|
|
19
|
-
if (container) {
|
|
20
|
-
const scrollTop = container.scrollTop
|
|
21
|
-
const clientHeight = container.clientHeight
|
|
22
|
-
cachedViewportTop = scrollTop - VIEWPORT_BUFFER_PX
|
|
23
|
-
cachedViewportBottom = scrollTop + clientHeight + VIEWPORT_BUFFER_PX
|
|
24
|
-
cachedViewportHeight = clientHeight
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const itemBottom = itemTop + itemHeight
|
|
28
|
-
return itemBottom >= cachedViewportTop && itemTop <= cachedViewportBottom
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function onEnter(el: HTMLElement, done: () => void) {
|
|
32
|
-
const left = parseInt(el.dataset.left || '0', 10)
|
|
33
|
-
const top = parseInt(el.dataset.top || '0', 10)
|
|
34
|
-
const index = parseInt(el.dataset.index || '0', 10)
|
|
35
|
-
// Get height from computed style or use a reasonable fallback
|
|
36
|
-
const height = el.offsetHeight || parseInt(getComputedStyle(el).height || '200', 10) || 200
|
|
37
|
-
|
|
38
|
-
// Skip animation during virtualization - just set position immediately
|
|
39
|
-
if (opts?.virtualizing?.value) {
|
|
40
|
-
el.style.transition = 'none'
|
|
41
|
-
el.style.opacity = '1'
|
|
42
|
-
el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
|
|
43
|
-
el.style.removeProperty('--masonry-opacity-delay')
|
|
44
|
-
el.style.transition = ''
|
|
45
|
-
done()
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Skip animation for items outside viewport - just set position immediately
|
|
50
|
-
if (!isItemInViewport(top, height)) {
|
|
51
|
-
el.style.opacity = '1'
|
|
52
|
-
el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
|
|
53
|
-
el.style.transition = 'none'
|
|
54
|
-
done()
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const delay = Math.min(index * 20, 160)
|
|
59
|
-
const prevOpacityDelay = el.style.getPropertyValue('--masonry-opacity-delay')
|
|
60
|
-
el.style.setProperty('--masonry-opacity-delay', `${delay}ms`)
|
|
61
|
-
|
|
62
|
-
el.style.opacity = '1'
|
|
63
|
-
el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
|
|
64
|
-
const clear = () => {
|
|
65
|
-
if (prevOpacityDelay) {
|
|
66
|
-
el.style.setProperty('--masonry-opacity-delay', prevOpacityDelay)
|
|
67
|
-
} else {
|
|
68
|
-
el.style.removeProperty('--masonry-opacity-delay')
|
|
69
|
-
}
|
|
70
|
-
el.removeEventListener('transitionend', clear)
|
|
71
|
-
done()
|
|
72
|
-
}
|
|
73
|
-
el.addEventListener('transitionend', clear)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function onBeforeEnter(el: HTMLElement) {
|
|
77
|
-
const left = parseInt(el.dataset.left || '0', 10)
|
|
78
|
-
const top = parseInt(el.dataset.top || '0', 10)
|
|
79
|
-
|
|
80
|
-
// Skip animation during virtualization
|
|
81
|
-
if (opts?.virtualizing?.value) {
|
|
82
|
-
el.style.transition = 'none'
|
|
83
|
-
el.style.opacity = '1'
|
|
84
|
-
el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
|
|
85
|
-
el.style.removeProperty('--masonry-opacity-delay')
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
el.style.opacity = '0'
|
|
90
|
-
el.style.transform = `translate3d(${left}px, ${top + 10}px, 0) scale(0.985)`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function onBeforeLeave(el: HTMLElement) {
|
|
94
|
-
const left = parseInt(el.dataset.left || '0', 10)
|
|
95
|
-
const top = parseInt(el.dataset.top || '0', 10)
|
|
96
|
-
const height = el.offsetHeight || parseInt(getComputedStyle(el).height || '200', 10) || 200
|
|
97
|
-
|
|
98
|
-
// Skip animation during virtualization
|
|
99
|
-
if (opts?.virtualizing?.value) {
|
|
100
|
-
// no-op; removal will be immediate in leave
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Skip animation for items outside viewport
|
|
105
|
-
if (!isItemInViewport(top, height)) {
|
|
106
|
-
el.style.transition = 'none'
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
el.style.transition = 'none'
|
|
111
|
-
el.style.opacity = '1'
|
|
112
|
-
el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
|
|
113
|
-
el.style.removeProperty('--masonry-opacity-delay')
|
|
114
|
-
el.style.transition = ''
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function onLeave(el: HTMLElement, done: () => void) {
|
|
118
|
-
const left = parseInt(el.dataset.left || '0', 10)
|
|
119
|
-
const top = parseInt(el.dataset.top || '0', 10)
|
|
120
|
-
const height = el.offsetHeight || parseInt(getComputedStyle(el).height || '200', 10) || 200
|
|
121
|
-
|
|
122
|
-
// Skip animation during virtualization - remove immediately
|
|
123
|
-
if (opts?.virtualizing?.value) {
|
|
124
|
-
done()
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Skip animation for items outside viewport - remove immediately
|
|
129
|
-
if (!isItemInViewport(top, height)) {
|
|
130
|
-
el.style.transition = 'none'
|
|
131
|
-
el.style.opacity = '0'
|
|
132
|
-
done()
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Prefer explicit option, fallback to CSS variable for safety
|
|
137
|
-
const fromOpts = typeof opts?.leaveDurationMs === 'number' ? opts!.leaveDurationMs : Number.NaN
|
|
138
|
-
let leaveMs = Number.isFinite(fromOpts) && fromOpts > 0 ? fromOpts : Number.NaN
|
|
139
|
-
if (!Number.isFinite(leaveMs)) {
|
|
140
|
-
const cs = getComputedStyle(el)
|
|
141
|
-
const varVal = cs.getPropertyValue('--masonry-leave-duration') || ''
|
|
142
|
-
const parsed = parseFloat(varVal)
|
|
143
|
-
leaveMs = Number.isFinite(parsed) && parsed > 0 ? parsed : 200
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const prevDuration = el.style.transitionDuration
|
|
147
|
-
|
|
148
|
-
const cleanup = () => {
|
|
149
|
-
el.removeEventListener('transitionend', onEnd as any)
|
|
150
|
-
clearTimeout(fallback)
|
|
151
|
-
el.style.transitionDuration = prevDuration || ''
|
|
152
|
-
}
|
|
153
|
-
const onEnd = (e?: Event) => {
|
|
154
|
-
if (!e || e.target === el) {
|
|
155
|
-
cleanup()
|
|
156
|
-
done()
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const fallback = setTimeout(() => {
|
|
160
|
-
cleanup()
|
|
161
|
-
done()
|
|
162
|
-
}, leaveMs + 100)
|
|
163
|
-
|
|
164
|
-
el.style.transitionDuration = `${leaveMs}ms`
|
|
165
|
-
el.style.opacity = '0'
|
|
166
|
-
el.style.transform = `translate3d(${left}px, ${top + 10}px, 0) scale(0.985)`
|
|
167
|
-
el.addEventListener('transitionend', onEnd as any)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
onEnter,
|
|
172
|
-
onBeforeEnter,
|
|
173
|
-
onBeforeLeave,
|
|
174
|
-
onLeave
|
|
175
|
-
}
|
|
176
|
-
}
|
package/src/main.ts
DELETED
package/src/masonryUtils.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type { LayoutOptions, ProcessedMasonryItem } from './types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Get responsive column count based on container width and layout sizes
|
|
5
|
-
*/
|
|
6
|
-
export function getColumnCount(layout: Pick<LayoutOptions, 'sizes'> & { sizes: Required<NonNullable<LayoutOptions['sizes']>> }, containerWidth?: number): number {
|
|
7
|
-
const width = containerWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
8
|
-
const sizes = layout.sizes
|
|
9
|
-
|
|
10
|
-
if (width >= 1536 && sizes['2xl']) return sizes['2xl']
|
|
11
|
-
if (width >= 1280 && sizes.xl) return sizes.xl
|
|
12
|
-
if (width >= 1024 && sizes.lg) return sizes.lg
|
|
13
|
-
if (width >= 768 && sizes.md) return sizes.md
|
|
14
|
-
if (width >= 640 && sizes.sm) return sizes.sm
|
|
15
|
-
return sizes.base
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Get current breakpoint name based on container width
|
|
20
|
-
*/
|
|
21
|
-
export function getBreakpointName(containerWidth?: number): 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' {
|
|
22
|
-
// Only use fallback if containerWidth is explicitly undefined/null
|
|
23
|
-
// If it's 0, we still use it (will return 'base')
|
|
24
|
-
const width = containerWidth !== undefined && containerWidth !== null
|
|
25
|
-
? containerWidth
|
|
26
|
-
: (typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
27
|
-
|
|
28
|
-
if (width >= 1536) return '2xl'
|
|
29
|
-
if (width >= 1280) return 'xl'
|
|
30
|
-
if (width >= 1024) return 'lg'
|
|
31
|
-
if (width >= 768) return 'md'
|
|
32
|
-
if (width >= 640) return 'sm'
|
|
33
|
-
return 'base'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Calculate container height based on item positions
|
|
38
|
-
*/
|
|
39
|
-
export function calculateContainerHeight(items: ProcessedMasonryItem[]): number {
|
|
40
|
-
const contentHeight = items.reduce((acc, item) => {
|
|
41
|
-
return Math.max(acc, item.top + item.columnHeight)
|
|
42
|
-
}, 0)
|
|
43
|
-
return contentHeight + 500
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Get style object for masonry item positioning
|
|
48
|
-
*/
|
|
49
|
-
export function getItemStyle(item: ProcessedMasonryItem): Record<string, string> {
|
|
50
|
-
return {
|
|
51
|
-
transform: `translate3d(${item.left}px, ${item.top}px, 0)` ,
|
|
52
|
-
top: '0px',
|
|
53
|
-
left: '0px',
|
|
54
|
-
width: `${item.columnWidth}px`,
|
|
55
|
-
height: `${item.columnHeight}px`
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Get item attributes for rendering
|
|
61
|
-
*/
|
|
62
|
-
export function getItemAttributes(item: ProcessedMasonryItem, index: number = 0): Record<string, any> {
|
|
63
|
-
return {
|
|
64
|
-
style: getItemStyle(item),
|
|
65
|
-
'data-top': item.top,
|
|
66
|
-
'data-left': item.left,
|
|
67
|
-
'data-id': `${(item as any).page}-${(item as any).id}`,
|
|
68
|
-
'data-index': index,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Calculate column heights for masonry layout
|
|
74
|
-
*/
|
|
75
|
-
export function calculateColumnHeights(items: ProcessedMasonryItem[], columnCount: number): number[] {
|
|
76
|
-
// Derive columns by actual left positions to reflect shortest-column placement
|
|
77
|
-
if (!items.length || columnCount <= 0) {
|
|
78
|
-
return new Array<number>(Math.max(1, columnCount)).fill(0)
|
|
79
|
-
}
|
|
80
|
-
// Unique lefts (sorted) represent the columns in visual order
|
|
81
|
-
const uniqueLefts = Array.from(new Set(items.map(i => i.left))).sort((a, b) => a - b)
|
|
82
|
-
const limitedLefts = uniqueLefts.slice(0, columnCount)
|
|
83
|
-
const leftIndexMap = new Map<number, number>()
|
|
84
|
-
for (let idx = 0; idx < limitedLefts.length; idx++) leftIndexMap.set(limitedLefts[idx], idx)
|
|
85
|
-
|
|
86
|
-
const heights = new Array<number>(limitedLefts.length).fill(0)
|
|
87
|
-
for (const it of items) {
|
|
88
|
-
const col = leftIndexMap.get(it.left)
|
|
89
|
-
if (col != null) {
|
|
90
|
-
heights[col] = Math.max(heights[col], it.top + it.columnHeight)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Pad if some columns haven't been populated yet (e.g., initial render)
|
|
94
|
-
while (heights.length < columnCount) heights.push(0)
|
|
95
|
-
return heights
|
|
96
|
-
}
|