@wyxos/vibe 1.6.3 → 1.6.4

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/vibe.css CHANGED
@@ -1 +1 @@
1
- .masonry-container[data-v-fa62094f]{overflow-anchor:none}.masonry-item[data-v-fa62094f]{will-change:transform,opacity;contain:layout paint;transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1)),opacity var(--masonry-leave-duration, .16s) ease-out var(--masonry-opacity-delay, 0ms);backface-visibility:hidden}.masonry-move[data-v-fa62094f]{transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1))}@media (prefers-reduced-motion: reduce){.masonry-container:not(.force-motion) .masonry-item[data-v-fa62094f],.masonry-container:not(.force-motion) .masonry-move[data-v-fa62094f]{transition-duration:1ms!important}}
1
+ .masonry-container[data-v-08b0e6d9]{overflow-anchor:none}.masonry-item[data-v-08b0e6d9]{will-change:transform,opacity;contain:layout paint;transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1)),opacity var(--masonry-leave-duration, .16s) ease-out var(--masonry-opacity-delay, 0ms);backface-visibility:hidden}.masonry-move[data-v-08b0e6d9]{transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1))}@media (prefers-reduced-motion: reduce){.masonry-container:not(.force-motion) .masonry-item[data-v-08b0e6d9],.masonry-container:not(.force-motion) .masonry-move[data-v-08b0e6d9]{transition-duration:1ms!important}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/vibe",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "main": "lib/index.js",
5
5
  "module": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
package/src/App.vue CHANGED
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import Masonry from "./Masonry.vue";
3
- import { ref } from "vue";
3
+ import { ref, reactive, computed } from "vue";
4
4
  import fixture from "./pages.json";
5
5
  import type { MasonryItem, GetPageResult } from "./types";
6
6
 
@@ -8,7 +8,26 @@ const items = ref<MasonryItem[]>([]);
8
8
 
9
9
  const masonry = ref<InstanceType<typeof Masonry> | null>(null);
10
10
 
11
- const layout = { sizes: { base: 1, sm: 2, md: 3, lg: 4, xl: 5, '2xl': 10 }, header: 40, footer: 40 };
11
+ const layoutParams = reactive({
12
+ sizes: {
13
+ base: 1,
14
+ sm: 2,
15
+ md: 3,
16
+ lg: 4,
17
+ xl: 5,
18
+ '2xl': 10
19
+ },
20
+ header: 0,
21
+ footer: 0
22
+ });
23
+
24
+ const layout = computed(() => ({
25
+ sizes: { ...layoutParams.sizes },
26
+ header: layoutParams.header,
27
+ footer: layoutParams.footer
28
+ }));
29
+
30
+ const showLayoutControls = ref(false);
12
31
 
13
32
  const getPage = async (page: number): Promise<GetPageResult> => {
14
33
  return new Promise((resolve) => {
@@ -36,28 +55,110 @@ const getPage = async (page: number): Promise<GetPageResult> => {
36
55
  };
37
56
  </script>
38
57
  <template>
39
- <main class="flex flex-col items-center p-4 bg-slate-100 h-screen overflow-hidden">
40
- <header class="sticky top-0 z-10 bg-slate-100 w-full p-4 flex flex-col items-center gap-4">
41
- <h1 class="text-2xl font-semibold mb-4">VIBE</h1>
42
- <p>Vue Infinite Block Engine</p>
43
-
44
- <p class="text-sm text-gray-500 text-center mb-4">
45
- 🚀 Built by <a href="https://wyxos.com" target="_blank" class="underline hover:text-black">wyxos.com</a> •
46
- 💾 <a href="https://github.com/wyxos/vibe" target="_blank" class="underline hover:text-black">Source on GitHub</a>
47
- </p>
48
-
49
- <div v-if="masonry" class="flex gap-4">
50
- <p>Loading: <span class="bg-blue-500 text-white p-2 rounded">{{ masonry.isLoading }}</span></p>
51
- <p>Showing: <span class="bg-blue-500 text-white p-2 rounded">{{ items.length }}</span></p>
58
+ <main class="flex flex-col h-screen overflow-hidden bg-slate-50 relative">
59
+ <!-- Floating Header -->
60
+ <header class="fixed top-4 left-0 right-0 z-20 w-full max-w-4xl mx-auto px-4 pointer-events-none">
61
+ <div class="bg-white/80 backdrop-blur-md border border-white/20 shadow-lg rounded-2xl p-4 flex items-center justify-between transition-all duration-300 hover:shadow-xl pointer-events-auto">
62
+ <div class="flex items-center gap-3">
63
+ <div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-xl flex items-center justify-center shadow-inner">
64
+ <i class="fas fa-layer-group text-white text-lg"></i>
65
+ </div>
66
+ <div>
67
+ <h1 class="text-lg font-bold text-slate-800 leading-tight">VIBE</h1>
68
+ <p class="text-xs text-slate-500 font-medium">Vue Infinite Block Engine</p>
69
+ </div>
70
+ </div>
71
+
72
+ <div class="flex items-center gap-4">
73
+ <div v-if="masonry" class="hidden md:flex items-center gap-3 text-sm font-medium text-slate-600 bg-slate-100/50 px-3 py-1.5 rounded-lg border border-slate-200/50">
74
+ <span class="flex items-center gap-1.5">
75
+ <span class="w-2 h-2 rounded-full" :class="masonry.isLoading ? 'bg-amber-400 animate-pulse' : 'bg-emerald-400'"></span>
76
+ {{ masonry.isLoading ? 'Loading...' : 'Ready' }}
77
+ </span>
78
+ <span class="w-px h-3 bg-slate-300"></span>
79
+ <span>{{ items.length }} items</span>
80
+ </div>
81
+
82
+ <button
83
+ @click="showLayoutControls = !showLayoutControls"
84
+ class="p-2 text-slate-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
85
+ :class="{ 'text-blue-600 bg-blue-50': showLayoutControls }"
86
+ title="Layout Controls"
87
+ >
88
+ <i class="fas fa-sliders"></i>
89
+ </button>
90
+
91
+ <a href="https://github.com/wyxos/vibe" target="_blank" class="p-2 text-slate-400 hover:text-slate-800 transition-colors" title="View on GitHub">
92
+ <i class="fab fa-github text-xl"></i>
93
+ </a>
94
+ </div>
52
95
  </div>
96
+
97
+ <!-- Layout Controls Panel -->
98
+ <transition
99
+ enter-active-class="transition duration-200 ease-out"
100
+ enter-from-class="transform -translate-y-2 opacity-0"
101
+ enter-to-class="transform translate-y-0 opacity-100"
102
+ leave-active-class="transition duration-150 ease-in"
103
+ leave-from-class="transform translate-y-0 opacity-100"
104
+ leave-to-class="transform -translate-y-2 opacity-0"
105
+ >
106
+ <div v-if="showLayoutControls" class="mt-2 bg-white/90 backdrop-blur-md border border-white/20 shadow-xl rounded-xl p-6 pointer-events-auto">
107
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
108
+ <!-- Column Settings -->
109
+ <div>
110
+ <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Column Configuration</h3>
111
+ <div class="grid grid-cols-3 sm:grid-cols-6 gap-3">
112
+ <div v-for="(val, key) in layoutParams.sizes" :key="key" class="flex flex-col gap-1.5">
113
+ <label class="text-[10px] font-bold text-slate-500 uppercase text-center">{{ key }}</label>
114
+ <input
115
+ v-model.number="layoutParams.sizes[key]"
116
+ type="number"
117
+ min="1"
118
+ class="w-full px-2 py-1.5 bg-slate-50 border border-slate-200 rounded-lg text-center text-sm font-medium text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
119
+ />
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Spacing Settings -->
125
+ <div>
126
+ <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Spacing</h3>
127
+ <div class="grid grid-cols-2 gap-4">
128
+ <div class="flex flex-col gap-1.5">
129
+ <label class="text-[10px] font-bold text-slate-500 uppercase">Header Offset</label>
130
+ <div class="relative">
131
+ <input
132
+ v-model.number="layoutParams.header"
133
+ type="number"
134
+ min="0"
135
+ class="w-full pl-3 pr-8 py-1.5 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
136
+ />
137
+ <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400">px</span>
138
+ </div>
139
+ </div>
140
+ <div class="flex flex-col gap-1.5">
141
+ <label class="text-[10px] font-bold text-slate-500 uppercase">Footer Offset</label>
142
+ <div class="relative">
143
+ <input
144
+ v-model.number="layoutParams.footer"
145
+ type="number"
146
+ min="0"
147
+ class="w-full pl-3 pr-8 py-1.5 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
148
+ />
149
+ <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400">px</span>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </transition>
53
157
  </header>
54
- <masonry class="bg-blue-500 " v-model:items="items" :get-next-page="getPage" :load-at-page="1" :layout="layout" ref="masonry">
55
- <template #item="{item, remove}">
56
- <img :src="item.src" class="w-full"/>
57
- <button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer" @click="remove(item)">
58
- <i class="fas fa-trash"></i>
59
- </button>
60
- </template>
61
- </masonry>
158
+
159
+ <!-- Main Content -->
160
+ <div class="flex flex-1 overflow-hidden relative pt-24">
161
+ <masonry v-model:items="items" :get-next-page="getPage" :load-at-page="1" :layout="layout" ref="masonry"></masonry>
162
+ </div>
62
163
  </main>
63
164
  </template>
package/src/Masonry.vue CHANGED
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
2
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
3
3
  import calculateLayout from "./calculateLayout";
4
4
  import { debounce } from 'lodash-es'
5
5
  import {
@@ -10,6 +10,7 @@ import {
10
10
  } from './masonryUtils'
11
11
  import { useMasonryTransitions } from './useMasonryTransitions'
12
12
  import { useMasonryScroll } from './useMasonryScroll'
13
+ import MasonryItem from './components/MasonryItem.vue'
13
14
 
14
15
  const props = defineProps({
15
16
  getNextPage: {
@@ -697,6 +698,18 @@ function init(items: any[], page: any, next: any) {
697
698
  updateScrollProgress()
698
699
  }
699
700
 
701
+ // Watch for layout changes and update columns + refresh layout dynamically
702
+ watch(
703
+ layout,
704
+ () => {
705
+ if (container.value) {
706
+ columns.value = getColumnCount(layout.value as any)
707
+ refreshLayout(masonry.value as any)
708
+ }
709
+ },
710
+ { deep: true }
711
+ )
712
+
700
713
  onMounted(async () => {
701
714
  try {
702
715
  columns.value = getColumnCount(layout.value as any)
@@ -738,13 +751,10 @@ onUnmounted(() => {
738
751
  @before-leave="beforeLeave">
739
752
  <div v-for="(item, i) in visibleMasonry" :key="`${item.page}-${item.id}`"
740
753
  class="absolute masonry-item"
741
- v-bind="getItemAttributes(item, i)">
754
+ v-bind="getItemAttributes(item, i)"
755
+ :style="{ paddingTop: `${layout.header}px`, paddingBottom: `${layout.footer}px` }">
742
756
  <slot name="item" v-bind="{item, remove}">
743
- <img :src="item.src" class="w-full" loading="lazy" decoding="async"/>
744
- <button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer"
745
- @click="remove(item)">
746
- <i class="fas fa-trash"></i>
747
- </button>
757
+ <MasonryItem :item="item" :remove="remove" />
748
758
  </slot>
749
759
  </div>
750
760
  </transition-group>
@@ -0,0 +1,132 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, watch } from 'vue';
3
+
4
+ const props = defineProps<{
5
+ item: any;
6
+ remove?: (item: any) => void;
7
+ }>();
8
+
9
+ const imageLoaded = ref(false);
10
+ const imageError = ref(false);
11
+ const imageSrc = ref<string | null>(null);
12
+
13
+ function preloadImage(src: string): Promise<void> {
14
+ return new Promise((resolve, reject) => {
15
+ if (!src) {
16
+ reject(new Error('No image source provided'));
17
+ return;
18
+ }
19
+
20
+ const img = new Image();
21
+ const startTime = Date.now();
22
+ const minLoadTime = 300; // Minimum time to show spinner (300ms)
23
+
24
+ img.onload = () => {
25
+ const elapsed = Date.now() - startTime;
26
+ const remaining = Math.max(0, minLoadTime - elapsed);
27
+
28
+ // Ensure spinner shows for at least minLoadTime
29
+ setTimeout(() => {
30
+ imageLoaded.value = true;
31
+ imageError.value = false;
32
+ resolve();
33
+ }, remaining);
34
+ };
35
+ img.onerror = () => {
36
+ imageError.value = true;
37
+ imageLoaded.value = false;
38
+ reject(new Error('Failed to load image'));
39
+ };
40
+ img.src = src;
41
+ });
42
+ }
43
+
44
+ onMounted(async () => {
45
+ // Debug: verify component is mounting
46
+ console.log('[MasonryItem] Component mounted', props.item?.id);
47
+
48
+ const src = props.item?.src;
49
+ if (src) {
50
+ imageSrc.value = src;
51
+ // Reset state to ensure spinner shows
52
+ imageLoaded.value = false;
53
+ imageError.value = false;
54
+ try {
55
+ await preloadImage(src);
56
+ } catch {
57
+ // Error handled by imageError state
58
+ }
59
+ }
60
+ });
61
+
62
+ watch(
63
+ () => props.item?.src,
64
+ async (newSrc) => {
65
+ if (newSrc && newSrc !== imageSrc.value) {
66
+ imageLoaded.value = false;
67
+ imageError.value = false;
68
+ imageSrc.value = newSrc;
69
+ try {
70
+ await preloadImage(newSrc);
71
+ } catch {
72
+ // Error handled by imageError state
73
+ }
74
+ }
75
+ }
76
+ );
77
+ </script>
78
+
79
+ <template>
80
+ <div class="relative w-full h-full group">
81
+ <!-- Custom slot content (replaces default if provided) -->
82
+ <slot :item="item" :remove="remove" :imageLoaded="imageLoaded" :imageError="imageError">
83
+ <!-- Default content when no slot is provided -->
84
+ <div class="w-full h-full rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 bg-white relative">
85
+ <!-- Spinner while loading -->
86
+ <div
87
+ v-if="!imageLoaded && !imageError"
88
+ class="absolute inset-0 flex items-center justify-center bg-slate-100"
89
+ >
90
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
91
+ </div>
92
+
93
+ <!-- Error state -->
94
+ <div
95
+ v-if="imageError"
96
+ class="absolute inset-0 flex flex-col items-center justify-center bg-slate-50 text-slate-400 text-sm p-4 text-center"
97
+ >
98
+ <i class="fas fa-image text-2xl mb-2 opacity-50"></i>
99
+ <span>Failed to load image</span>
100
+ </div>
101
+
102
+ <!-- Image (only shown when loaded) -->
103
+ <img
104
+ v-if="imageLoaded && imageSrc"
105
+ :src="imageSrc"
106
+ class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
107
+ loading="lazy"
108
+ decoding="async"
109
+ />
110
+
111
+ <!-- Overlay Gradient -->
112
+ <div class="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
113
+
114
+ <!-- Remove button -->
115
+ <button
116
+ v-if="remove"
117
+ class="absolute top-2 right-2 w-8 h-8 flex items-center justify-center bg-white/90 backdrop-blur-sm text-slate-700 rounded-full shadow-sm opacity-0 group-hover:opacity-100 transform translate-y-2 group-hover:translate-y-0 transition-all duration-300 hover:bg-red-500 hover:text-white cursor-pointer"
118
+ @click.stop="remove(item)"
119
+ aria-label="Remove item"
120
+ >
121
+ <i class="fas fa-times text-sm"></i>
122
+ </button>
123
+
124
+ <!-- Item Info (Optional, visible on hover) -->
125
+ <div class="absolute bottom-0 left-0 right-0 p-3 opacity-0 group-hover:opacity-100 transform translate-y-2 group-hover:translate-y-0 transition-all duration-300 delay-75">
126
+ <p class="text-white text-xs font-medium truncate drop-shadow-md">Item #{{ String(item.id).split('-')[0] }}</p>
127
+ </div>
128
+ </div>
129
+ </slot>
130
+ </div>
131
+ </template>
132
+
package/src/style.css CHANGED
@@ -1,2 +1,32 @@
1
1
  @import "tailwindcss";
2
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
+ }