@wyxos/vibe 1.6.12 → 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.
@@ -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
+ }
@@ -1,20 +1,275 @@
1
- <template>
2
- <div class="min-h-screen bg-slate-50 p-8 pt-20">
3
- <div class="max-w-4xl mx-auto">
4
- <h1 class="text-4xl font-bold text-slate-800 mb-6">Examples</h1>
5
- <p class="text-slate-600 mb-8">This is a placeholder for examples page.</p>
6
-
7
- <div class="bg-white rounded-xl shadow-sm p-6">
8
- <h2 class="text-2xl font-semibold text-slate-800 mb-4">Coming Soon</h2>
9
- <p class="text-slate-600">
10
- Examples and code snippets will be added here soon.
11
- </p>
12
- </div>
13
- </div>
14
- </div>
15
- </template>
16
-
17
- <script setup lang="ts">
18
- // Examples page placeholder
19
- </script>
20
-
1
+ <template>
2
+ <div class="min-h-screen bg-slate-50 pt-20">
3
+ <div class="max-w-7xl mx-auto px-4 py-8">
4
+ <div class="flex gap-8">
5
+ <!-- Side Menu -->
6
+ <aside class="hidden lg:block w-64 flex-shrink-0 sticky top-24 h-fit">
7
+ <nav class="bg-white rounded-lg shadow-sm p-4 border border-slate-200">
8
+ <h3 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-4">Examples</h3>
9
+ <ul class="space-y-2">
10
+ <li v-for="example in examples" :key="example.id">
11
+ <a
12
+ :href="`#${example.id}`"
13
+ @click.prevent="scrollTo(example.id)"
14
+ class="block px-3 py-2 text-sm rounded-md transition-colors"
15
+ :class="activeSection === example.id
16
+ ? 'bg-blue-50 text-blue-600 font-medium'
17
+ : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'"
18
+ >
19
+ {{ example.title }}
20
+ </a>
21
+ </li>
22
+ </ul>
23
+ </nav>
24
+ </aside>
25
+
26
+ <!-- Main Content -->
27
+ <main class="flex-1 min-w-0">
28
+ <div class="space-y-16">
29
+ <!-- Basic Example -->
30
+ <section id="basic" ref="basicRef" class="scroll-mt-24">
31
+ <div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
32
+ <div class="px-6 py-4 border-b border-slate-200 bg-slate-50">
33
+ <h2 class="text-2xl font-bold text-slate-800">Basic Usage</h2>
34
+ <p class="text-slate-600 mt-1">Simple masonry grid with default MasonryItem component</p>
35
+ </div>
36
+
37
+ <!-- Live Example -->
38
+ <div class="p-6 bg-slate-50">
39
+ <div class="bg-white rounded-lg border border-slate-200 p-4" style="height: 500px;">
40
+ <BasicExample />
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Code Tabs -->
45
+ <div class="px-6 pb-6">
46
+ <CodeTabs
47
+ vue='<script setup>
48
+ import { ref } from "vue";
49
+ import { Masonry } from "@wyxos/vibe";
50
+
51
+ const items = ref([]);
52
+
53
+ async function getNextPage(page) {
54
+ const response = await fetch(`/api/items?page=${page}`);
55
+ const data = await response.json();
56
+ return {
57
+ items: data.items,
58
+ nextPage: page + 1
59
+ };
60
+ }
61
+ </script>
62
+
63
+ <template>
64
+ <Masonry
65
+ v-model:items="items"
66
+ :get-next-page="getNextPage"
67
+ :load-at-page="1"
68
+ />
69
+ </template>'
70
+ />
71
+ </div>
72
+ </div>
73
+ </section>
74
+
75
+ <!-- Custom Masonry Item Example -->
76
+ <section id="custom-item" ref="customItemRef" class="scroll-mt-24">
77
+ <div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
78
+ <div class="px-6 py-4 border-b border-slate-200 bg-slate-50">
79
+ <h2 class="text-2xl font-bold text-slate-800">Custom Masonry Item</h2>
80
+ <p class="text-slate-600 mt-1">Customize item rendering with scoped slots</p>
81
+ </div>
82
+
83
+ <!-- Live Example -->
84
+ <div class="p-6 bg-slate-50">
85
+ <div class="bg-white rounded-lg border border-slate-200 p-4" style="height: 500px;">
86
+ <CustomItemExample />
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Code Tabs -->
91
+ <div class="px-6 pb-6">
92
+ <CodeTabs
93
+ vue='<script setup>
94
+ import { ref } from "vue";
95
+ import { Masonry } from "@wyxos/vibe";
96
+
97
+ const items = ref([]);
98
+
99
+ async function getNextPage(page) {
100
+ const response = await fetch(`/api/items?page=${page}`);
101
+ const data = await response.json();
102
+ return {
103
+ items: data.items,
104
+ nextPage: page + 1
105
+ };
106
+ }
107
+ </script>
108
+
109
+ <template>
110
+ <Masonry
111
+ v-model:items="items"
112
+ :get-next-page="getNextPage"
113
+ :load-at-page="1"
114
+ >
115
+ <template #item="{ item, remove }">
116
+ <div class="custom-card">
117
+ <img :src="item.src" :alt="item.title" />
118
+ <div class="overlay">
119
+ <h3>{{ item.title }}</h3>
120
+ <button @click="remove">Remove</button>
121
+ </div>
122
+ </div>
123
+ </template>
124
+ </Masonry>
125
+ </template>'
126
+ />
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. -->
151
+ </div>
152
+ </section>
153
+
154
+ <!-- Swipe Mode Example -->
155
+ <section id="swipe-mode" ref="swipeModeRef" class="scroll-mt-24">
156
+ <div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
157
+ <div class="px-6 py-4 border-b border-slate-200 bg-slate-50">
158
+ <h2 class="text-2xl font-bold text-slate-800">Swipe Mode</h2>
159
+ <p class="text-slate-600 mt-1">Vertical swipe feed for mobile devices</p>
160
+ </div>
161
+
162
+ <!-- Live Example -->
163
+ <div class="p-6 bg-slate-50">
164
+ <div class="bg-white rounded-lg border border-slate-200 p-4" style="height: 500px;">
165
+ <SwipeModeExample />
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Code Tabs -->
170
+ <div class="px-6 pb-6">
171
+ <CodeTabs
172
+ vue='<script setup>
173
+ import { ref } from "vue";
174
+ import { Masonry } from "@wyxos/vibe";
175
+
176
+ const items = ref([]);
177
+
178
+ async function getNextPage(page) {
179
+ const response = await fetch(`/api/items?page=${page}`);
180
+ const data = await response.json();
181
+ return {
182
+ items: data.items,
183
+ nextPage: page + 1
184
+ };
185
+ }
186
+ </script>
187
+
188
+ <template>
189
+ <!-- Auto mode: switches to swipe on mobile -->
190
+ <Masonry
191
+ v-model:items="items"
192
+ :get-next-page="getNextPage"
193
+ layout-mode="auto"
194
+ :mobile-breakpoint="768"
195
+ />
196
+
197
+ <!-- Force swipe mode on all devices -->
198
+ <Masonry
199
+ v-model:items="items"
200
+ :get-next-page="getNextPage"
201
+ layout-mode="swipe"
202
+ />
203
+ </template>'
204
+ />
205
+ </div>
206
+ </div>
207
+ </section>
208
+ </div>
209
+ </main>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </template>
214
+
215
+ <script setup lang="ts">
216
+ // @ts-nocheck - documentation page with inline string code samples that Vue/TS can't statically analyze cleanly
217
+ import { ref, onMounted, onUnmounted } from 'vue'
218
+ import CodeTabs from '../components/CodeTabs.vue'
219
+ import BasicExample from '../components/examples/BasicExample.vue'
220
+ import CustomItemExample from '../components/examples/CustomItemExample.vue'
221
+ import SwipeModeExample from '../components/examples/SwipeModeExample.vue'
222
+ import HeaderFooterExample from '../components/examples/HeaderFooterExample.vue'
223
+
224
+ const examples = [
225
+ { id: 'basic', title: 'Basic Usage' },
226
+ { id: 'custom-item', title: 'Custom Masonry Item' },
227
+ { id: 'header-footer', title: 'Header & Footer' },
228
+ { id: 'swipe-mode', title: 'Swipe Mode' }
229
+ ]
230
+
231
+ const activeSection = ref('basic')
232
+ const basicRef = ref<HTMLElement | null>(null)
233
+ const customItemRef = ref<HTMLElement | null>(null)
234
+ const headerFooterRef = ref<HTMLElement | null>(null)
235
+ const swipeModeRef = ref<HTMLElement | null>(null)
236
+
237
+ function scrollTo(id: string) {
238
+ const element = document.getElementById(id)
239
+ if (element) {
240
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' })
241
+ activeSection.value = id
242
+ }
243
+ }
244
+
245
+ function updateActiveSection() {
246
+ const sections = [
247
+ { id: 'basic', ref: basicRef },
248
+ { id: 'custom-item', ref: customItemRef },
249
+ { id: 'header-footer', ref: headerFooterRef },
250
+ { id: 'swipe-mode', ref: swipeModeRef }
251
+ ]
252
+
253
+ const scrollPosition = window.scrollY + 100
254
+
255
+ for (let i = sections.length - 1; i >= 0; i--) {
256
+ const section = sections[i]
257
+ if (section.ref.value) {
258
+ const top = section.ref.value.offsetTop
259
+ if (scrollPosition >= top) {
260
+ activeSection.value = section.id
261
+ break
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ onMounted(() => {
268
+ window.addEventListener('scroll', updateActiveSection)
269
+ updateActiveSection()
270
+ })
271
+
272
+ onUnmounted(() => {
273
+ window.removeEventListener('scroll', updateActiveSection)
274
+ })
275
+ </script>