@wyxos/vibe 1.6.13 → 1.6.14
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 +808 -713
- package/lib/vibe.css +1 -1
- package/package.json +1 -2
- package/src/Masonry.vue +1233 -1195
- package/src/components/MasonryItem.vue +431 -346
- package/src/components/examples/HeaderFooterExample.vue +78 -0
- package/src/useMasonryScroll.ts +60 -60
- package/src/views/Examples.vue +28 -0
- package/src/views/Home.vue +236 -208
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Masonry
|
|
3
|
+
v-model:items="items"
|
|
4
|
+
:get-next-page="getPage"
|
|
5
|
+
:load-at-page="1"
|
|
6
|
+
:layout="layout"
|
|
7
|
+
>
|
|
8
|
+
<template #item-header="{ item }">
|
|
9
|
+
<div class="h-full flex items-center justify-between px-3">
|
|
10
|
+
<div class="flex items-center gap-2">
|
|
11
|
+
<div class="w-6 h-6 rounded-full bg-white/80 backdrop-blur-sm flex items-center justify-center shadow-sm">
|
|
12
|
+
<i :class="item.type === 'video' ? 'fas fa-video text-[10px] text-slate-500' : 'fas fa-image text-[10px] text-slate-500'"></i>
|
|
13
|
+
</div>
|
|
14
|
+
<span class="text-xs font-medium text-slate-700">#{{ String(item.id).split('-')[0] }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<span v-if="item.title" class="text-[11px] text-slate-600 truncate max-w-[160px]">{{ item.title }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<template #item-footer="{ item, remove }">
|
|
21
|
+
<div class="h-full flex items-center justify-between px-3">
|
|
22
|
+
<div class="flex items-center gap-2">
|
|
23
|
+
<button
|
|
24
|
+
v-if="remove"
|
|
25
|
+
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"
|
|
26
|
+
@click.stop="remove(item)"
|
|
27
|
+
>
|
|
28
|
+
Remove
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="text-[11px] text-slate-600">
|
|
32
|
+
{{ item.width }}×{{ item.height }}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
</Masonry>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { ref } from 'vue'
|
|
41
|
+
import Masonry from '../../Masonry.vue'
|
|
42
|
+
import fixture from '../../pages.json'
|
|
43
|
+
import type { MasonryItem, GetPageResult } from '../../types'
|
|
44
|
+
|
|
45
|
+
const items = ref<MasonryItem[]>([])
|
|
46
|
+
|
|
47
|
+
const layout = {
|
|
48
|
+
sizes: { base: 1, sm: 2, md: 3, lg: 4 },
|
|
49
|
+
gutterX: 10,
|
|
50
|
+
gutterY: 10,
|
|
51
|
+
header: 36,
|
|
52
|
+
footer: 40
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const getPage = async (page: number): Promise<GetPageResult> => {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined
|
|
59
|
+
|
|
60
|
+
if (!pageData) {
|
|
61
|
+
resolve({
|
|
62
|
+
items: [],
|
|
63
|
+
nextPage: null
|
|
64
|
+
})
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resolve({
|
|
69
|
+
items: pageData.items,
|
|
70
|
+
nextPage: page < (fixture as any[]).length ? page + 1 : null
|
|
71
|
+
})
|
|
72
|
+
}, 300)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
package/src/useMasonryScroll.ts
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { nextTick, type Ref } from 'vue'
|
|
2
|
-
import { calculateColumnHeights } from './masonryUtils'
|
|
3
|
-
import type { ProcessedMasonryItem } from './types'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Composable for handling masonry scroll behavior and item cleanup
|
|
7
|
-
*/
|
|
8
|
-
export function useMasonryScroll({
|
|
9
|
-
container,
|
|
10
|
-
masonry,
|
|
11
|
-
columns,
|
|
12
|
-
containerHeight,
|
|
13
|
-
isLoading,
|
|
14
|
-
pageSize,
|
|
15
|
-
refreshLayout,
|
|
16
|
-
setItemsRaw,
|
|
17
|
-
loadNext,
|
|
18
|
-
loadThresholdPx
|
|
19
|
-
}: {
|
|
20
|
-
container: Ref<HTMLElement | null>
|
|
21
|
-
masonry: Ref<ProcessedMasonryItem[]>
|
|
22
|
-
columns: Ref<number>
|
|
23
|
-
containerHeight: Ref<number>
|
|
24
|
-
isLoading: Ref<boolean>
|
|
25
|
-
pageSize: number
|
|
26
|
-
refreshLayout: (items: ProcessedMasonryItem[]) => void
|
|
27
|
-
setItemsRaw: (items: ProcessedMasonryItem[]) => void
|
|
28
|
-
loadNext: () => Promise<any>
|
|
29
|
-
loadThresholdPx?: number
|
|
30
|
-
}) {
|
|
31
|
-
let cleanupInProgress = false
|
|
32
|
-
let lastScrollTop = 0
|
|
33
|
-
|
|
34
|
-
async function handleScroll(precomputedHeights?: number[]) {
|
|
35
|
-
if (!container.value) return
|
|
36
|
-
|
|
37
|
-
const columnHeights = precomputedHeights ?? calculateColumnHeights(masonry.value, columns.value)
|
|
38
|
-
const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
|
|
39
|
-
const scrollerBottom = container.value.scrollTop + container.value.clientHeight
|
|
40
|
-
|
|
41
|
-
const isScrollingDown = container.value.scrollTop > lastScrollTop + 1 // tolerate tiny jitter
|
|
42
|
-
lastScrollTop = container.value.scrollTop
|
|
43
|
-
|
|
44
|
-
const threshold = typeof loadThresholdPx === 'number' ? loadThresholdPx : 200
|
|
45
|
-
const triggerPoint = threshold >= 0
|
|
46
|
-
? Math.max(0, tallest - threshold)
|
|
47
|
-
: Math.max(0, tallest + threshold)
|
|
48
|
-
const nearBottom = scrollerBottom >= triggerPoint
|
|
49
|
-
|
|
50
|
-
if (nearBottom && isScrollingDown && !isLoading.value) {
|
|
51
|
-
await loadNext()
|
|
52
|
-
await nextTick()
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
handleScroll
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
import { nextTick, type Ref } from 'vue'
|
|
2
|
+
import { calculateColumnHeights } from './masonryUtils'
|
|
3
|
+
import type { ProcessedMasonryItem } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Composable for handling masonry scroll behavior and item cleanup
|
|
7
|
+
*/
|
|
8
|
+
export function useMasonryScroll({
|
|
9
|
+
container,
|
|
10
|
+
masonry,
|
|
11
|
+
columns,
|
|
12
|
+
containerHeight,
|
|
13
|
+
isLoading,
|
|
14
|
+
pageSize,
|
|
15
|
+
refreshLayout,
|
|
16
|
+
setItemsRaw,
|
|
17
|
+
loadNext,
|
|
18
|
+
loadThresholdPx
|
|
19
|
+
}: {
|
|
20
|
+
container: Ref<HTMLElement | null>
|
|
21
|
+
masonry: Ref<ProcessedMasonryItem[]>
|
|
22
|
+
columns: Ref<number>
|
|
23
|
+
containerHeight: Ref<number>
|
|
24
|
+
isLoading: Ref<boolean>
|
|
25
|
+
pageSize: number
|
|
26
|
+
refreshLayout: (items: ProcessedMasonryItem[]) => void
|
|
27
|
+
setItemsRaw: (items: ProcessedMasonryItem[]) => void
|
|
28
|
+
loadNext: () => Promise<any>
|
|
29
|
+
loadThresholdPx?: number
|
|
30
|
+
}) {
|
|
31
|
+
let cleanupInProgress = false
|
|
32
|
+
let lastScrollTop = 0
|
|
33
|
+
|
|
34
|
+
async function handleScroll(precomputedHeights?: number[]) {
|
|
35
|
+
if (!container.value) return
|
|
36
|
+
|
|
37
|
+
const columnHeights = precomputedHeights ?? calculateColumnHeights(masonry.value, columns.value)
|
|
38
|
+
const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
|
|
39
|
+
const scrollerBottom = container.value.scrollTop + container.value.clientHeight
|
|
40
|
+
|
|
41
|
+
const isScrollingDown = container.value.scrollTop > lastScrollTop + 1 // tolerate tiny jitter
|
|
42
|
+
lastScrollTop = container.value.scrollTop
|
|
43
|
+
|
|
44
|
+
const threshold = typeof loadThresholdPx === 'number' ? loadThresholdPx : 200
|
|
45
|
+
const triggerPoint = threshold >= 0
|
|
46
|
+
? Math.max(0, tallest - threshold)
|
|
47
|
+
: Math.max(0, tallest + threshold)
|
|
48
|
+
const nearBottom = scrollerBottom >= triggerPoint
|
|
49
|
+
|
|
50
|
+
if (nearBottom && isScrollingDown && !isLoading.value) {
|
|
51
|
+
await loadNext()
|
|
52
|
+
await nextTick()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
handleScroll
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/views/Examples.vue
CHANGED
|
@@ -125,6 +125,29 @@ async function getNextPage(page) {
|
|
|
125
125
|
</template>'
|
|
126
126
|
/>
|
|
127
127
|
</div>
|
|
128
|
+
-->
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
<!-- Header & Footer Example -->
|
|
133
|
+
<section id="header-footer" ref="headerFooterRef" class="scroll-mt-24">
|
|
134
|
+
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
135
|
+
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50">
|
|
136
|
+
<h2 class="text-2xl font-bold text-slate-800">Header & Footer</h2>
|
|
137
|
+
<p class="text-slate-600 mt-1">
|
|
138
|
+
Use `layout.header` and `layout.footer` with slots to add per-item UI like badges, titles and actions.
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Live Example -->
|
|
143
|
+
<div class="p-6 bg-slate-50">
|
|
144
|
+
<div class="bg-white rounded-lg border border-slate-200 p-4" style="height: 500px;">
|
|
145
|
+
<HeaderFooterExample />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Code Tabs omitted for this example to keep docs build-safe.
|
|
150
|
+
See the Header & Footer example component for full code. -->
|
|
128
151
|
</div>
|
|
129
152
|
</section>
|
|
130
153
|
|
|
@@ -190,21 +213,25 @@ async function getNextPage(page) {
|
|
|
190
213
|
</template>
|
|
191
214
|
|
|
192
215
|
<script setup lang="ts">
|
|
216
|
+
// @ts-nocheck - documentation page with inline string code samples that Vue/TS can't statically analyze cleanly
|
|
193
217
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
194
218
|
import CodeTabs from '../components/CodeTabs.vue'
|
|
195
219
|
import BasicExample from '../components/examples/BasicExample.vue'
|
|
196
220
|
import CustomItemExample from '../components/examples/CustomItemExample.vue'
|
|
197
221
|
import SwipeModeExample from '../components/examples/SwipeModeExample.vue'
|
|
222
|
+
import HeaderFooterExample from '../components/examples/HeaderFooterExample.vue'
|
|
198
223
|
|
|
199
224
|
const examples = [
|
|
200
225
|
{ id: 'basic', title: 'Basic Usage' },
|
|
201
226
|
{ id: 'custom-item', title: 'Custom Masonry Item' },
|
|
227
|
+
{ id: 'header-footer', title: 'Header & Footer' },
|
|
202
228
|
{ id: 'swipe-mode', title: 'Swipe Mode' }
|
|
203
229
|
]
|
|
204
230
|
|
|
205
231
|
const activeSection = ref('basic')
|
|
206
232
|
const basicRef = ref<HTMLElement | null>(null)
|
|
207
233
|
const customItemRef = ref<HTMLElement | null>(null)
|
|
234
|
+
const headerFooterRef = ref<HTMLElement | null>(null)
|
|
208
235
|
const swipeModeRef = ref<HTMLElement | null>(null)
|
|
209
236
|
|
|
210
237
|
function scrollTo(id: string) {
|
|
@@ -219,6 +246,7 @@ function updateActiveSection() {
|
|
|
219
246
|
const sections = [
|
|
220
247
|
{ id: 'basic', ref: basicRef },
|
|
221
248
|
{ id: 'custom-item', ref: customItemRef },
|
|
249
|
+
{ id: 'header-footer', ref: headerFooterRef },
|
|
222
250
|
{ id: 'swipe-mode', ref: swipeModeRef }
|
|
223
251
|
]
|
|
224
252
|
|