@wyxos/vibe 1.6.14 → 1.6.16

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.
@@ -15,6 +15,24 @@ export function getColumnCount(layout: Pick<LayoutOptions, 'sizes'> & { sizes: R
15
15
  return sizes.base
16
16
  }
17
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
+
18
36
  /**
19
37
  * Calculate container height based on item positions
20
38
  */
@@ -1,236 +1,321 @@
1
- <template>
2
- <main class="flex flex-col h-screen overflow-hidden bg-slate-50 relative pt-[112px]">
3
- <!-- Fixed Sub-Header -->
4
- <header class="fixed top-[53px] left-0 right-0 z-20 w-full bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm transition-all duration-300">
5
- <div class="max-w-7xl mx-auto px-4 h-14 flex items-center justify-end">
6
- <!-- Right: Controls -->
7
- <div class="flex items-center gap-3">
8
- <!-- Status Pill -->
9
- <div v-if="masonry" class="hidden md:flex items-center gap-2 text-xs font-medium text-slate-600 bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-100">
10
- <span class="flex items-center gap-1.5">
11
- <span class="w-1.5 h-1.5 rounded-full shadow-sm" :class="masonry.isLoading ? 'bg-amber-400 animate-pulse' : 'bg-emerald-400'"></span>
12
- {{ masonry.isLoading ? 'Loading...' : 'Ready' }}
13
- </span>
14
- <span class="w-px h-3 bg-slate-200"></span>
15
- <span>{{ items.length }} items</span>
16
- </div>
17
-
18
- <div class="h-8 w-px bg-slate-100 mx-1 hidden md:block"></div>
19
-
20
- <!-- Action Buttons -->
21
- <div class="flex items-center gap-1">
22
- <button
23
- @click="showLayoutControls = !showLayoutControls"
24
- class="w-9 h-9 flex items-center justify-center text-slate-500 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all duration-200"
25
- :class="{ 'text-blue-600 bg-blue-50 ring-2 ring-blue-100': showLayoutControls }"
26
- title="Layout Settings"
27
- >
28
- <i class="fas fa-sliders text-sm"></i>
29
- </button>
30
-
31
- <a
32
- href="https://github.com/wyxos/vibe"
33
- target="_blank"
34
- class="w-9 h-9 flex items-center justify-center text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-xl transition-all duration-200"
35
- title="View on GitHub"
36
- >
37
- <i class="fab fa-github text-lg"></i>
38
- </a>
39
- </div>
40
- </div>
41
- </div>
42
-
43
- <!-- Layout Controls Panel -->
44
- <transition
45
- enter-active-class="transition duration-200 ease-out"
46
- enter-from-class="transform -translate-y-2 opacity-0"
47
- enter-to-class="transform translate-y-0 opacity-100"
48
- leave-active-class="transition duration-150 ease-in"
49
- leave-from-class="transform translate-y-0 opacity-100"
50
- leave-to-class="transform -translate-y-2 opacity-0"
51
- >
52
- <div v-if="showLayoutControls" class="absolute top-full left-4 right-4 md:left-auto md:right-4 mt-2 md:w-full md:max-w-lg bg-white/90 backdrop-blur-md border border-slate-200 shadow-xl rounded-xl p-4 md:p-6 pointer-events-auto z-30">
53
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
54
- <!-- Column Settings -->
55
- <div>
56
- <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Column Configuration</h3>
57
- <div class="grid grid-cols-3 sm:grid-cols-6 gap-2 sm:gap-3">
58
- <div v-for="(val, key) in layoutParams.sizes" :key="key" class="flex flex-col gap-1.5">
59
- <label class="text-[10px] font-bold text-slate-500 uppercase text-center">{{ key }}</label>
60
- <input
61
- v-model.number="layoutParams.sizes[key]"
62
- type="number"
63
- min="1"
64
- class="w-full px-1 py-2 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"
65
- />
66
- </div>
67
- </div>
68
- </div>
69
-
70
- <!-- Spacing Settings -->
71
- <div>
72
- <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Spacing</h3>
73
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
74
- <div class="flex flex-col gap-1.5">
75
- <label class="text-[10px] font-bold text-slate-500 uppercase">Header Offset</label>
76
- <div class="relative">
77
- <input
78
- v-model.number="layoutParams.header"
79
- type="number"
80
- min="0"
81
- class="w-full pl-3 pr-8 py-2 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"
82
- />
83
- <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400">px</span>
84
- </div>
85
- </div>
86
- <div class="flex flex-col gap-1.5">
87
- <label class="text-[10px] font-bold text-slate-500 uppercase">Footer Offset</label>
88
- <div class="relative">
89
- <input
90
- v-model.number="layoutParams.footer"
91
- type="number"
92
- min="0"
93
- class="w-full pl-3 pr-8 py-2 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"
94
- />
95
- <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400">px</span>
96
- </div>
97
- </div>
98
- </div>
99
- </div>
100
-
101
- <!-- Device Simulation -->
102
- <div class="md:col-span-2 border-t border-slate-100 pt-6 mt-2">
103
- <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Device Simulation</h3>
104
- <div class="flex flex-wrap gap-2">
105
- <button
106
- v-for="mode in ['auto', 'phone', 'tablet', 'desktop']"
107
- :key="mode"
108
- @click="deviceMode = mode as any"
109
- class="px-4 py-2 rounded-lg text-sm font-medium transition-all capitalize"
110
- :class="deviceMode === mode ? 'bg-blue-600 text-white shadow-md shadow-blue-200' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'"
111
- >
112
- <i class="fas mr-2" :class="{
113
- 'fa-desktop': mode === 'desktop' || mode === 'auto',
114
- 'fa-mobile-alt': mode === 'phone',
115
- 'fa-tablet-alt': mode === 'tablet'
116
- }"></i>
117
- {{ mode }}
118
- </button>
119
- </div>
120
- </div>
121
- </div>
122
- </div>
123
- </transition>
124
- </header>
125
-
126
- <!-- Main Content -->
127
- <div class="flex flex-1 overflow-hidden relative p-5 transition-all duration-300 ease-in-out" :class="{'bg-slate-200/50': deviceMode !== 'auto'}">
128
- <div :style="containerStyle" class="transition-all duration-500 ease-in-out bg-slate-50 shadow-sm relative">
129
- <masonry v-model:items="items" :get-next-page="getPage" :load-at-page="1" :layout="layout" ref="masonry">
130
- <!-- Demonstrate header/footer customization in the main demo -->
131
- <template #item-header="{ item }">
132
- <div class="h-full flex items-center justify-between px-3">
133
- <div class="flex items-center gap-2">
134
- <div class="w-6 h-6 rounded-full bg-white/80 backdrop-blur-sm flex items-center justify-center shadow-sm">
135
- <i :class="item.type === 'video' ? 'fas fa-video text-[10px] text-slate-500' : 'fas fa-image text-[10px] text-slate-500'"></i>
136
- </div>
137
- <span class="text-xs font-medium text-slate-700">#{{ String(item.id).split('-')[0] }}</span>
138
- </div>
139
- <span v-if="item.title" class="text-[11px] text-slate-600 truncate max-w-[160px]">
140
- {{ item.title }}
141
- </span>
142
- </div>
143
- </template>
144
-
145
- <template #item-footer="{ item, remove }">
146
- <div class="h-full flex items-center justify-between px-3">
147
- <button
148
- v-if="remove"
149
- 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"
150
- @click.stop="remove(item)"
151
- >
152
- Remove
153
- </button>
154
- <div class="text-[11px] text-slate-600">
155
- {{ item.width }}×{{ item.height }}
156
- </div>
157
- </div>
158
- </template>
159
- </masonry>
160
- </div>
161
- </div>
162
- </main>
163
- </template>
164
-
165
- <script setup lang="ts">
166
- import Masonry from "../Masonry.vue";
167
- import { ref, reactive, computed } from "vue";
168
- import fixture from "../pages.json";
169
- import type { MasonryItem, GetPageResult } from "../types";
170
-
171
- const items = ref<MasonryItem[]>([]);
172
-
173
- const masonry = ref<InstanceType<typeof Masonry> | null>(null);
174
-
175
- const layoutParams = reactive({
176
- sizes: {
177
- base: 1,
178
- sm: 2,
179
- md: 3,
180
- lg: 4,
181
- xl: 5,
182
- '2xl': 10
183
- },
184
- header: 36,
185
- footer: 40
186
- });
187
-
188
- const layout = computed(() => ({
189
- sizes: { ...layoutParams.sizes },
190
- header: layoutParams.header,
191
- footer: layoutParams.footer
192
- }));
193
-
194
- const showLayoutControls = ref(false);
195
-
196
- const getPage = async (page: number): Promise<GetPageResult> => {
197
- return new Promise((resolve) => {
198
- setTimeout(() => {
199
- // Check if the page exists in the fixture
200
- const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined;
201
-
202
- if (!pageData) {
203
- // Return empty items if page doesn't exist
204
- resolve({
205
- items: [],
206
- nextPage: null // null indicates no more pages
207
- });
208
- return;
209
- }
210
-
211
- const output: GetPageResult = {
212
- items: pageData.items,
213
- nextPage: page < (fixture as any[]).length ? page + 1 : null
214
- };
215
-
216
- resolve(output);
217
- }, 1000);
218
- });
219
- };
220
-
221
- // Device Simulation
222
- const deviceMode = ref<'auto' | 'phone' | 'tablet' | 'desktop'>('auto');
223
-
224
- const containerStyle = computed(() => {
225
- switch (deviceMode.value) {
226
- case 'phone':
227
- return { width: '375px', maxWidth: '100%', margin: '0 auto', border: '1px solid #e2e8f0', borderRadius: '20px', overflow: 'hidden', height: '100%' };
228
- case 'tablet':
229
- return { width: '768px', maxWidth: '100%', margin: '0 auto', border: '1px solid #e2e8f0', borderRadius: '12px', overflow: 'hidden', height: '100%' };
230
- case 'desktop':
231
- return { width: '1280px', maxWidth: '100%', margin: '0 auto', border: '1px solid #e2e8f0', borderRadius: '8px', overflow: 'hidden', height: '100%' };
232
- default:
233
- return { width: '100%', height: '100%' };
234
- }
235
- });
236
- </script>
1
+ <template>
2
+ <main class="flex flex-col h-screen overflow-hidden bg-slate-50 relative pt-[112px]">
3
+ <!-- Fixed Sub-Header -->
4
+ <header
5
+ class="fixed top-[53px] left-0 right-0 z-20 w-full bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm transition-all duration-300">
6
+ <div class="max-w-7xl mx-auto px-4 h-14 flex items-center justify-end">
7
+ <!-- Right: Controls -->
8
+ <div class="flex items-center gap-3">
9
+ <!-- Status Pill -->
10
+ <div v-if="masonry"
11
+ class="hidden md:flex items-center gap-2 text-xs font-medium text-slate-600 bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-100">
12
+ <span class="flex items-center gap-1.5">
13
+ <span class="w-1.5 h-1.5 rounded-full shadow-sm"
14
+ :class="masonry.isLoading ? 'bg-amber-400 animate-pulse' : 'bg-emerald-400'"></span>
15
+ {{ masonry.isLoading ? 'Loading...' : 'Ready' }}
16
+ </span>
17
+ <span class="w-px h-3 bg-slate-200"></span>
18
+ <span>{{ items.length }} items</span>
19
+ <span class="w-px h-3 bg-slate-200"></span>
20
+ <span class="uppercase font-semibold text-slate-500">{{ masonry.currentBreakpoint }}</span>
21
+ <span class="w-px h-3 bg-slate-200"></span>
22
+ <span>{{ Math.round(masonry.containerWidth) }}×{{ Math.round(masonry.containerHeight) }}</span>
23
+ <span v-if="masonry.currentPage != null" class="w-px h-3 bg-slate-200"></span>
24
+ <span v-if="masonry.currentPage != null" class="text-slate-500">Page {{ masonry.currentPage }}</span>
25
+ </div>
26
+
27
+ <div class="h-8 w-px bg-slate-100 mx-1 hidden md:block"></div>
28
+
29
+ <!-- Action Buttons -->
30
+ <div class="flex items-center gap-1">
31
+ <button @click="showLayoutControls = !showLayoutControls"
32
+ class="w-9 h-9 flex items-center justify-center text-slate-500 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all duration-200"
33
+ :class="{ 'text-blue-600 bg-blue-50 ring-2 ring-blue-100': showLayoutControls }" title="Layout Settings">
34
+ <i class="fas fa-sliders text-sm"></i>
35
+ </button>
36
+
37
+ <a href="https://github.com/wyxos/vibe" target="_blank"
38
+ class="w-9 h-9 flex items-center justify-center text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-xl transition-all duration-200"
39
+ title="View on GitHub">
40
+ <i class="fab fa-github text-lg"></i>
41
+ </a>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Layout Controls Panel -->
47
+ <transition enter-active-class="transition duration-200 ease-out"
48
+ enter-from-class="transform -translate-y-2 opacity-0" enter-to-class="transform translate-y-0 opacity-100"
49
+ leave-active-class="transition duration-150 ease-in" leave-from-class="transform translate-y-0 opacity-100"
50
+ leave-to-class="transform -translate-y-2 opacity-0">
51
+ <div v-if="showLayoutControls"
52
+ class="absolute top-full left-4 right-4 md:left-auto md:right-4 mt-2 md:w-full md:max-w-lg bg-white/90 backdrop-blur-md border border-slate-200 shadow-xl rounded-xl p-4 md:p-6 pointer-events-auto z-30">
53
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
54
+ <!-- Column Settings -->
55
+ <div>
56
+ <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Column Configuration</h3>
57
+ <div class="grid grid-cols-3 gap-2 sm:gap-3">
58
+ <div v-for="(val, key) in layoutParams.sizes" :key="key" class="flex flex-col gap-1.5">
59
+ <label class="text-[10px] font-bold text-slate-500 uppercase text-center">{{ key }}</label>
60
+ <input v-model.number="layoutParams.sizes[key]" type="number" min="1"
61
+ class="w-full px-1 py-2 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" />
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Spacing Settings -->
67
+ <div>
68
+ <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Spacing</h3>
69
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
70
+ <div class="flex flex-col gap-1.5">
71
+ <label class="text-[10px] font-bold text-slate-500 uppercase">Header Offset</label>
72
+ <div class="relative">
73
+ <input v-model.number="layoutParams.header" type="number" min="0"
74
+ class="w-full pl-3 pr-8 py-2 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" />
75
+ <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400">px</span>
76
+ </div>
77
+ </div>
78
+ <div class="flex flex-col gap-1.5">
79
+ <label class="text-[10px] font-bold text-slate-500 uppercase">Footer Offset</label>
80
+ <div class="relative">
81
+ <input v-model.number="layoutParams.footer" type="number" min="0"
82
+ class="w-full pl-3 pr-8 py-2 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" />
83
+ <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400">px</span>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Device Simulation -->
90
+ <div class="md:col-span-2 border-t border-slate-100 pt-6 mt-2">
91
+ <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Device Simulation</h3>
92
+ <div class="space-y-4">
93
+ <div class="flex flex-wrap gap-2">
94
+ <button v-for="mode in ['auto', 'phone', 'tablet', 'desktop']" :key="mode"
95
+ @click="deviceMode = mode as any"
96
+ class="px-4 py-2 rounded-lg text-sm font-medium transition-all capitalize"
97
+ :class="deviceMode === mode ? 'bg-blue-600 text-white shadow-md shadow-blue-200' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'">
98
+ <i class="fas mr-2" :class="{
99
+ 'fa-desktop': mode === 'desktop' || mode === 'auto',
100
+ 'fa-mobile-alt': mode === 'phone',
101
+ 'fa-tablet-alt': mode === 'tablet'
102
+ }"></i>
103
+ {{ mode }}
104
+ </button>
105
+ </div>
106
+ <div v-if="deviceMode !== 'auto'" class="flex flex-wrap gap-2">
107
+ <button v-for="orientation in ['portrait', 'landscape']" :key="orientation"
108
+ @click="deviceOrientation = orientation as any"
109
+ class="px-4 py-2 rounded-lg text-sm font-medium transition-all capitalize"
110
+ :class="deviceOrientation === orientation ? 'bg-blue-600 text-white shadow-md shadow-blue-200' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'">
111
+ <i class="fas mr-2" :class="{
112
+ 'fa-mobile-alt': orientation === 'portrait',
113
+ 'fa-mobile-alt fa-rotate-90': orientation === 'landscape'
114
+ }"></i>
115
+ {{ orientation }}
116
+ </button>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </transition>
123
+ </header>
124
+
125
+ <!-- Main Content -->
126
+ <div class="flex flex-1 overflow-hidden relative p-5 transition-all duration-300 ease-in-out"
127
+ :class="{ 'bg-slate-200/50': deviceMode !== 'auto' }">
128
+ <div :style="containerStyle" class="transition-all duration-500 ease-in-out bg-slate-50 shadow-sm relative">
129
+ <masonry v-model:items="items" :get-next-page="getPage" :load-at-page="1" :layout="layout"
130
+ :layout-mode="deviceMode === 'phone' || deviceMode === 'tablet' ? 'swipe' : 'auto'" ref="masonry"
131
+ class="demo-masonry">
132
+ <template #item-footer="{ item, remove }">
133
+ <div class="h-full flex items-center justify-between px-3">
134
+ <button v-if="remove"
135
+ 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"
136
+ @click.stop="remove(item)">
137
+ Remove
138
+ </button>
139
+ <div class="text-[11px] text-slate-600">
140
+ {{ item.width }}×{{ item.height }}
141
+ </div>
142
+ </div>
143
+ </template>
144
+ </masonry>
145
+ </div>
146
+ </div>
147
+ </main>
148
+ </template>
149
+
150
+ <script setup lang="ts">
151
+ import Masonry from "../Masonry.vue";
152
+ import { ref, reactive, computed, watch, nextTick } from "vue";
153
+ import fixture from "../pages.json";
154
+ import type { MasonryItem, GetPageResult } from "../types";
155
+
156
+ const items = ref<MasonryItem[]>([]);
157
+
158
+ const masonry = ref<InstanceType<typeof Masonry> | null>(null);
159
+
160
+ const layoutParams = reactive({
161
+ sizes: {
162
+ base: 1,
163
+ sm: 2,
164
+ md: 3,
165
+ lg: 4,
166
+ xl: 5,
167
+ '2xl': 10
168
+ },
169
+ header: 0,
170
+ footer: 40
171
+ });
172
+
173
+ const layout = computed(() => ({
174
+ sizes: { ...layoutParams.sizes },
175
+ header: layoutParams.header,
176
+ footer: layoutParams.footer
177
+ }));
178
+
179
+ const showLayoutControls = ref(false);
180
+
181
+ const getPage = async (page: number): Promise<GetPageResult> => {
182
+ return new Promise((resolve) => {
183
+ setTimeout(() => {
184
+ // Check if the page exists in the fixture
185
+ const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined;
186
+
187
+ if (!pageData) {
188
+ // Return empty items if page doesn't exist
189
+ resolve({
190
+ items: [],
191
+ nextPage: null // null indicates no more pages
192
+ });
193
+ return;
194
+ }
195
+
196
+ const output: GetPageResult = {
197
+ items: pageData.items,
198
+ nextPage: page < (fixture as any[]).length ? page + 1 : null
199
+ };
200
+
201
+ resolve(output);
202
+ }, 1000);
203
+ });
204
+ };
205
+
206
+ // Device Simulation
207
+ const deviceMode = ref<'auto' | 'phone' | 'tablet' | 'desktop'>('auto');
208
+ const deviceOrientation = ref<'portrait' | 'landscape'>('portrait');
209
+
210
+ const deviceDimensions = computed(() => {
211
+ let baseDimensions: { width: number; height: number } | null = null;
212
+
213
+ switch (deviceMode.value) {
214
+ case 'phone':
215
+ baseDimensions = { width: 375, height: 667 };
216
+ break;
217
+ case 'tablet':
218
+ baseDimensions = { width: 768, height: 1024 };
219
+ break;
220
+ case 'desktop':
221
+ // Desktop is typically landscape, but allow portrait too
222
+ baseDimensions = { width: 1280, height: 720 };
223
+ break;
224
+ default:
225
+ return null;
226
+ }
227
+
228
+ // Swap dimensions for landscape orientation (except desktop which is already landscape by default)
229
+ if (deviceOrientation.value === 'landscape' && deviceMode.value !== 'desktop') {
230
+ return { width: baseDimensions.height, height: baseDimensions.width };
231
+ }
232
+
233
+ // For desktop, swap if portrait is selected
234
+ if (deviceMode.value === 'desktop' && deviceOrientation.value === 'portrait') {
235
+ return { width: baseDimensions.height, height: baseDimensions.width };
236
+ }
237
+
238
+ return baseDimensions;
239
+ });
240
+
241
+ const containerStyle = computed(() => {
242
+ const dimensions = deviceDimensions.value;
243
+ if (!dimensions) {
244
+ return { width: '100%', height: '100%' };
245
+ }
246
+
247
+ const borderRadius = deviceMode.value === 'phone' ? '20px' : deviceMode.value === 'tablet' ? '12px' : '8px';
248
+
249
+ return {
250
+ width: `${dimensions.width}px`,
251
+ height: `${dimensions.height}px`,
252
+ maxWidth: '100%',
253
+ margin: '0 auto',
254
+ border: '1px solid #e2e8f0',
255
+ borderRadius,
256
+ overflow: 'hidden'
257
+ };
258
+ });
259
+
260
+ // Override container dimensions when device simulation is active
261
+ watch([deviceMode, deviceOrientation, masonry], () => {
262
+ if (masonry.value && deviceDimensions.value) {
263
+ // Use nextTick to ensure masonry is fully initialized
264
+ nextTick(() => {
265
+ if (masonry.value && masonry.value.setFixedDimensions) {
266
+ masonry.value.setFixedDimensions({
267
+ width: deviceDimensions.value.width,
268
+ height: deviceDimensions.value.height
269
+ });
270
+ }
271
+ });
272
+ } else if (masonry.value && !deviceDimensions.value) {
273
+ // Clear fixed dimensions when in auto mode
274
+ nextTick(() => {
275
+ if (masonry.value && masonry.value.setFixedDimensions) {
276
+ masonry.value.setFixedDimensions(null);
277
+ }
278
+ });
279
+ }
280
+ }, { immediate: true });
281
+
282
+ // Add hover overlays to masonry items
283
+ watch(() => items.value, () => {
284
+ nextTick(() => {
285
+ const masonryItems = document.querySelectorAll('.demo-masonry .masonry-item');
286
+ masonryItems.forEach((el) => {
287
+ if ((el as HTMLElement).hasAttribute('data-overlay-added')) return;
288
+
289
+ const itemId = el.getAttribute('data-id');
290
+ if (itemId) {
291
+ const item = items.value.find((i: any) => `${i.page}-${i.id}` === itemId);
292
+ if (item) {
293
+ const container = el.querySelector('.relative.w-full.h-full.flex.flex-col') as HTMLElement;
294
+ if (container && !container.querySelector('.demo-item-overlay')) {
295
+ const overlay = document.createElement('div');
296
+ overlay.className = 'demo-item-overlay absolute top-2 left-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none';
297
+ overlay.innerHTML = `
298
+ <div class="flex items-center gap-2 px-2 py-1 rounded-lg bg-black/70 backdrop-blur-sm text-white text-xs">
299
+ <div class="w-4 h-4 rounded-full bg-white/20 flex items-center justify-center">
300
+ <i class="fas ${item.type === 'video' ? 'fa-video' : 'fa-image'} text-[8px]"></i>
301
+ </div>
302
+ <span class="font-medium">#${String(item.id).split('-')[0]}</span>
303
+ </div>
304
+ `;
305
+ container.classList.add('group');
306
+ container.appendChild(overlay);
307
+ (el as HTMLElement).setAttribute('data-overlay-added', 'true');
308
+ }
309
+ }
310
+ }
311
+ });
312
+ });
313
+ }, { deep: true });
314
+ </script>
315
+
316
+ <style scoped>
317
+ /* Hover overlay for masonry items */
318
+ .demo-masonry :deep(.masonry-item:hover .demo-item-overlay) {
319
+ opacity: 1;
320
+ }
321
+ </style>