@wyxos/vibe 1.6.5 → 1.6.7
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 +1 -1
- package/lib/apple-touch-icon-114x114.png +0 -0
- package/lib/apple-touch-icon-120x120.png +0 -0
- package/lib/apple-touch-icon-144x144.png +0 -0
- package/lib/apple-touch-icon-152x152.png +0 -0
- package/lib/apple-touch-icon-180x180.png +0 -0
- package/lib/apple-touch-icon-57x57.png +0 -0
- package/lib/apple-touch-icon-60x60.png +0 -0
- package/lib/apple-touch-icon-72x72.png +0 -0
- package/lib/apple-touch-icon-76x76.png +0 -0
- package/lib/favicon-128x128.png +0 -0
- package/lib/favicon-16x16.png +0 -0
- package/lib/favicon-256x256.png +0 -0
- package/lib/favicon-32x32.png +0 -0
- package/lib/favicon-48x48.png +0 -0
- package/lib/favicon-64x64.png +0 -0
- package/lib/favicon.ico +0 -0
- package/lib/index.js +538 -470
- package/lib/logo-dark.svg +36 -0
- package/lib/logo-light.svg +29 -0
- package/lib/logo.svg +32 -0
- package/lib/manifest.json +41 -0
- package/lib/vibe.css +1 -1
- package/package.json +5 -2
- package/src/App.vue +30 -159
- package/src/Masonry.vue +3 -2
- package/src/components/MasonryItem.vue +123 -21
- package/src/main.ts +2 -1
- package/src/pages.json +36401 -32501
- package/src/router/index.ts +20 -0
- package/src/style.css +31 -31
- package/src/views/Examples.vue +20 -0
- package/src/views/Home.vue +169 -0
- package/lib/demo.webp +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="vibeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
|
|
5
|
+
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:1" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="vibeGradLight" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
8
|
+
<stop offset="0%" style="stop-color:#60a5fa;stop-opacity:1" />
|
|
9
|
+
<stop offset="100%" style="stop-color:#22d3ee;stop-opacity:1" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
|
|
13
|
+
<!-- Background -->
|
|
14
|
+
<rect width="120" height="120" rx="24" fill="url(#vibeGrad)"/>
|
|
15
|
+
|
|
16
|
+
<!-- Masonry blocks - representing the infinite block engine -->
|
|
17
|
+
<!-- Left column -->
|
|
18
|
+
<rect x="18" y="20" width="24" height="32" rx="5" fill="white" opacity="0.95"/>
|
|
19
|
+
<rect x="18" y="58" width="24" height="20" rx="5" fill="white" opacity="0.85"/>
|
|
20
|
+
<rect x="18" y="84" width="24" height="16" rx="5" fill="white" opacity="0.9"/>
|
|
21
|
+
|
|
22
|
+
<!-- Middle column -->
|
|
23
|
+
<rect x="48" y="28" width="24" height="22" rx="5" fill="white" opacity="0.9"/>
|
|
24
|
+
<rect x="48" y="56" width="24" height="36" rx="5" fill="white" opacity="0.95"/>
|
|
25
|
+
<rect x="48" y="98" width="24" height="14" rx="5" fill="white" opacity="0.8"/>
|
|
26
|
+
|
|
27
|
+
<!-- Right column -->
|
|
28
|
+
<rect x="78" y="22" width="24" height="38" rx="5" fill="white" opacity="0.85"/>
|
|
29
|
+
<rect x="78" y="66" width="24" height="24" rx="5" fill="white" opacity="0.9"/>
|
|
30
|
+
<rect x="78" y="96" width="24" height="16" rx="5" fill="white" opacity="0.85"/>
|
|
31
|
+
|
|
32
|
+
<!-- Infinite loop symbol (subtle overlay) -->
|
|
33
|
+
<path d="M 30 60 Q 40 50, 50 60 Q 60 70, 70 60" stroke="url(#vibeGradLight)" stroke-width="3" fill="none" opacity="0.4" stroke-linecap="round"/>
|
|
34
|
+
<path d="M 30 60 Q 40 70, 50 60 Q 60 50, 70 60" stroke="url(#vibeGradLight)" stroke-width="3" fill="none" opacity="0.4" stroke-linecap="round"/>
|
|
35
|
+
</svg>
|
|
36
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="vibeGradLight" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
|
|
5
|
+
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:1" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<!-- Masonry blocks - representing the infinite block engine -->
|
|
10
|
+
<!-- Left column -->
|
|
11
|
+
<rect x="18" y="20" width="24" height="32" rx="5" fill="url(#vibeGradLight)" opacity="0.95"/>
|
|
12
|
+
<rect x="18" y="58" width="24" height="20" rx="5" fill="url(#vibeGradLight)" opacity="0.85"/>
|
|
13
|
+
<rect x="18" y="84" width="24" height="16" rx="5" fill="url(#vibeGradLight)" opacity="0.9"/>
|
|
14
|
+
|
|
15
|
+
<!-- Middle column -->
|
|
16
|
+
<rect x="48" y="28" width="24" height="22" rx="5" fill="url(#vibeGradLight)" opacity="0.9"/>
|
|
17
|
+
<rect x="48" y="56" width="24" height="36" rx="5" fill="url(#vibeGradLight)" opacity="0.95"/>
|
|
18
|
+
<rect x="48" y="98" width="24" height="14" rx="5" fill="url(#vibeGradLight)" opacity="0.8"/>
|
|
19
|
+
|
|
20
|
+
<!-- Right column -->
|
|
21
|
+
<rect x="78" y="22" width="24" height="38" rx="5" fill="url(#vibeGradLight)" opacity="0.85"/>
|
|
22
|
+
<rect x="78" y="66" width="24" height="24" rx="5" fill="url(#vibeGradLight)" opacity="0.9"/>
|
|
23
|
+
<rect x="78" y="96" width="24" height="16" rx="5" fill="url(#vibeGradLight)" opacity="0.85"/>
|
|
24
|
+
|
|
25
|
+
<!-- Infinite loop symbol (subtle overlay) -->
|
|
26
|
+
<path d="M 30 60 Q 40 50, 50 60 Q 60 70, 70 60" stroke="url(#vibeGradLight)" stroke-width="3" fill="none" opacity="0.6" stroke-linecap="round"/>
|
|
27
|
+
<path d="M 30 60 Q 40 70, 50 60 Q 60 50, 70 60" stroke="url(#vibeGradLight)" stroke-width="3" fill="none" opacity="0.6" stroke-linecap="round"/>
|
|
28
|
+
</svg>
|
|
29
|
+
|
package/lib/logo.svg
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
|
|
5
|
+
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:1" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<!-- Background circle -->
|
|
10
|
+
<circle cx="60" cy="60" r="58" fill="url(#grad1)" opacity="0.1"/>
|
|
11
|
+
|
|
12
|
+
<!-- Masonry blocks representing infinite layout -->
|
|
13
|
+
<!-- Column 1 -->
|
|
14
|
+
<rect x="20" y="15" width="22" height="28" rx="4" fill="url(#grad1)" opacity="0.9"/>
|
|
15
|
+
<rect x="20" y="48" width="22" height="18" rx="4" fill="url(#grad1)" opacity="0.7"/>
|
|
16
|
+
<rect x="20" y="71" width="22" height="34" rx="4" fill="url(#grad1)" opacity="0.8"/>
|
|
17
|
+
|
|
18
|
+
<!-- Column 2 -->
|
|
19
|
+
<rect x="49" y="25" width="22" height="20" rx="4" fill="url(#grad1)" opacity="0.8"/>
|
|
20
|
+
<rect x="49" y="50" width="22" height="32" rx="4" fill="url(#grad1)" opacity="0.9"/>
|
|
21
|
+
<rect x="49" y="87" width="22" height="18" rx="4" fill="url(#grad1)" opacity="0.6"/>
|
|
22
|
+
|
|
23
|
+
<!-- Column 3 -->
|
|
24
|
+
<rect x="78" y="18" width="22" height="35" rx="4" fill="url(#grad1)" opacity="0.7"/>
|
|
25
|
+
<rect x="78" y="58" width="22" height="22" rx="4" fill="url(#grad1)" opacity="0.9"/>
|
|
26
|
+
<rect x="78" y="85" width="22" height="20" rx="4" fill="url(#grad1)" opacity="0.8"/>
|
|
27
|
+
|
|
28
|
+
<!-- Infinite symbol overlay (subtle) -->
|
|
29
|
+
<path d="M 35 60 Q 45 50, 55 60 Q 65 70, 75 60" stroke="url(#grad1)" stroke-width="2.5" fill="none" opacity="0.6" stroke-linecap="round"/>
|
|
30
|
+
<path d="M 35 60 Q 45 70, 55 60 Q 65 50, 75 60" stroke="url(#grad1)" stroke-width="2.5" fill="none" opacity="0.6" stroke-linecap="round"/>
|
|
31
|
+
</svg>
|
|
32
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "VIBE - Vue Infinite Block Engine",
|
|
3
|
+
"short_name": "VIBE",
|
|
4
|
+
"description": "A high-performance, responsive masonry layout engine for Vue 3",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#f8fafc",
|
|
8
|
+
"theme_color": "#3b82f6",
|
|
9
|
+
"icons": [
|
|
10
|
+
{
|
|
11
|
+
"src": "favicon-16x16.png",
|
|
12
|
+
"sizes": "16x16",
|
|
13
|
+
"type": "image/png"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "favicon-32x32.png",
|
|
17
|
+
"sizes": "32x32",
|
|
18
|
+
"type": "image/png"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"src": "favicon-48x48.png",
|
|
22
|
+
"sizes": "48x48",
|
|
23
|
+
"type": "image/png"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"src": "favicon-64x64.png",
|
|
27
|
+
"sizes": "64x64",
|
|
28
|
+
"type": "image/png"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"src": "favicon-128x128.png",
|
|
32
|
+
"sizes": "128x128",
|
|
33
|
+
"type": "image/png"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"src": "favicon-256x256.png",
|
|
37
|
+
"sizes": "256x256",
|
|
38
|
+
"type": "image/png"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
package/lib/vibe.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.masonry-container[data-v-
|
|
1
|
+
.masonry-container[data-v-110c3294]{overflow-anchor:none}.masonry-item[data-v-110c3294]{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-110c3294]{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-110c3294],.masonry-container:not(.force-motion) .masonry-move[data-v-110c3294]{transition-duration:1ms!important}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wyxos/vibe",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"module": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"lodash": "^4.17.21",
|
|
40
40
|
"lodash-es": "^4.17.21",
|
|
41
|
-
"vue": "^3.0.0"
|
|
41
|
+
"vue": "^3.0.0",
|
|
42
|
+
"vue-router": "^4.6.3"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"vue": "^3.0.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
48
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
47
49
|
"@tailwindcss/vite": "^4.0.15",
|
|
48
50
|
"@types/lodash-es": "^4.17.12",
|
|
49
51
|
"@types/node": "^24.5.2",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"chalk": "^5.3.0",
|
|
53
55
|
"inquirer": "^10.1.8",
|
|
54
56
|
"jsdom": "^26.0.0",
|
|
57
|
+
"sharp": "^0.34.5",
|
|
55
58
|
"simple-git": "^3.27.0",
|
|
56
59
|
"tailwindcss": "^4.0.15",
|
|
57
60
|
"typescript": "^5.9.2",
|
package/src/App.vue
CHANGED
|
@@ -1,164 +1,35 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import Masonry from "./Masonry.vue";
|
|
3
|
-
import { ref, reactive, computed } from "vue";
|
|
4
|
-
import fixture from "./pages.json";
|
|
5
|
-
import type { MasonryItem, GetPageResult } from "./types";
|
|
6
|
-
|
|
7
|
-
const items = ref<MasonryItem[]>([]);
|
|
8
|
-
|
|
9
|
-
const masonry = ref<InstanceType<typeof Masonry> | null>(null);
|
|
10
|
-
|
|
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);
|
|
31
|
-
|
|
32
|
-
const getPage = async (page: number): Promise<GetPageResult> => {
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
// Check if the page exists in the fixture
|
|
36
|
-
const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined;
|
|
37
|
-
|
|
38
|
-
if (!pageData) {
|
|
39
|
-
// Return empty items if page doesn't exist
|
|
40
|
-
resolve({
|
|
41
|
-
items: [],
|
|
42
|
-
nextPage: null // null indicates no more pages
|
|
43
|
-
});
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const output: GetPageResult = {
|
|
48
|
-
items: pageData.items,
|
|
49
|
-
nextPage: page < (fixture as any[]).length ? page + 1 : null
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
resolve(output);
|
|
53
|
-
}, 1000);
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
</script>
|
|
57
1
|
<template>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
</
|
|
66
|
-
<div>
|
|
67
|
-
<
|
|
68
|
-
|
|
2
|
+
<div id="app">
|
|
3
|
+
<nav class="fixed top-0 left-0 right-0 z-30 bg-white/90 backdrop-blur-md border-b border-slate-200 shadow-sm">
|
|
4
|
+
<div class="max-w-7xl mx-auto px-4 py-3">
|
|
5
|
+
<div class="flex items-center justify-between">
|
|
6
|
+
<router-link to="/" class="flex items-center gap-2 text-lg font-bold text-slate-800 hover:text-blue-600 transition-colors">
|
|
7
|
+
<img src="/logo-light.svg" alt="VIBE" class="w-8 h-8" />
|
|
8
|
+
<span>VIBE</span>
|
|
9
|
+
</router-link>
|
|
10
|
+
<div class="flex items-center gap-4">
|
|
11
|
+
<router-link
|
|
12
|
+
to="/"
|
|
13
|
+
class="text-sm text-slate-600 hover:text-slate-800 transition-colors"
|
|
14
|
+
active-class="text-blue-600 font-medium"
|
|
15
|
+
>
|
|
16
|
+
Demo
|
|
17
|
+
</router-link>
|
|
18
|
+
<router-link
|
|
19
|
+
to="/examples"
|
|
20
|
+
class="text-sm text-slate-600 hover:text-slate-800 transition-colors"
|
|
21
|
+
active-class="text-blue-600 font-medium"
|
|
22
|
+
>
|
|
23
|
+
Examples
|
|
24
|
+
</router-link>
|
|
69
25
|
</div>
|
|
70
26
|
</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>
|
|
95
27
|
</div>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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>
|
|
157
|
-
</header>
|
|
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>
|
|
163
|
-
</main>
|
|
28
|
+
</nav>
|
|
29
|
+
<router-view />
|
|
30
|
+
</div>
|
|
164
31
|
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
// Root App component with router
|
|
35
|
+
</script>
|
package/src/Masonry.vue
CHANGED
|
@@ -96,7 +96,7 @@ const props = defineProps({
|
|
|
96
96
|
autoRefreshOnEmpty: {
|
|
97
97
|
type: Boolean,
|
|
98
98
|
default: false
|
|
99
|
-
}
|
|
99
|
+
},
|
|
100
100
|
})
|
|
101
101
|
|
|
102
102
|
const defaultLayout = {
|
|
@@ -753,7 +753,8 @@ onUnmounted(() => {
|
|
|
753
753
|
class="absolute masonry-item"
|
|
754
754
|
v-bind="getItemAttributes(item, i)"
|
|
755
755
|
:style="{ paddingTop: `${layout.header}px`, paddingBottom: `${layout.footer}px` }">
|
|
756
|
-
|
|
756
|
+
<!-- Use default slot if provided, otherwise use MasonryItem -->
|
|
757
|
+
<slot :item="item" :remove="remove">
|
|
757
758
|
<MasonryItem :item="item" :remove="remove" />
|
|
758
759
|
</slot>
|
|
759
760
|
</div>
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, watch } from 'vue';
|
|
2
|
+
import { ref, onMounted, watch, computed, withDefaults } from 'vue';
|
|
3
3
|
|
|
4
|
-
const props = defineProps<{
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
5
|
item: any;
|
|
6
6
|
remove?: (item: any) => void;
|
|
7
|
-
|
|
7
|
+
type?: 'image' | 'video';
|
|
8
|
+
notFound?: boolean;
|
|
9
|
+
}>(), {
|
|
10
|
+
// Auto-read from item if not explicitly provided
|
|
11
|
+
type: undefined,
|
|
12
|
+
notFound: undefined
|
|
13
|
+
});
|
|
8
14
|
|
|
9
15
|
const imageLoaded = ref(false);
|
|
10
16
|
const imageError = ref(false);
|
|
11
17
|
const imageSrc = ref<string | null>(null);
|
|
18
|
+
const videoLoaded = ref(false);
|
|
19
|
+
const videoError = ref(false);
|
|
20
|
+
const videoSrc = ref<string | null>(null);
|
|
21
|
+
// Auto-read from props or item object, default to 'image'
|
|
22
|
+
const mediaType = computed(() => props.type ?? props.item?.type ?? 'image');
|
|
23
|
+
const showNotFound = computed(() => props.notFound ?? props.item?.notFound ?? false);
|
|
12
24
|
|
|
13
25
|
function preloadImage(src: string): Promise<void> {
|
|
14
26
|
return new Promise((resolve, reject) => {
|
|
@@ -41,14 +53,64 @@ function preloadImage(src: string): Promise<void> {
|
|
|
41
53
|
});
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
function preloadVideo(src: string): Promise<void> {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
if (!src) {
|
|
59
|
+
reject(new Error('No video source provided'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const video = document.createElement('video');
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
const minLoadTime = 300;
|
|
66
|
+
|
|
67
|
+
video.preload = 'metadata';
|
|
68
|
+
video.muted = true; // Muted for autoplay compatibility
|
|
69
|
+
|
|
70
|
+
video.onloadedmetadata = () => {
|
|
71
|
+
const elapsed = Date.now() - startTime;
|
|
72
|
+
const remaining = Math.max(0, minLoadTime - elapsed);
|
|
73
|
+
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
videoLoaded.value = true;
|
|
76
|
+
videoError.value = false;
|
|
77
|
+
resolve();
|
|
78
|
+
}, remaining);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
video.onerror = () => {
|
|
82
|
+
videoError.value = true;
|
|
83
|
+
videoLoaded.value = false;
|
|
84
|
+
reject(new Error('Failed to load video'));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
video.src = src;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
44
91
|
onMounted(async () => {
|
|
45
92
|
// Debug: verify component is mounting
|
|
46
93
|
console.log('[MasonryItem] Component mounted', props.item?.id);
|
|
47
94
|
|
|
95
|
+
// If notFound is true, skip preloading
|
|
96
|
+
if (showNotFound.value) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
48
100
|
const src = props.item?.src;
|
|
49
|
-
if (src)
|
|
101
|
+
if (!src) return;
|
|
102
|
+
|
|
103
|
+
if (mediaType.value === 'video') {
|
|
104
|
+
videoSrc.value = src;
|
|
105
|
+
videoLoaded.value = false;
|
|
106
|
+
videoError.value = false;
|
|
107
|
+
try {
|
|
108
|
+
await preloadVideo(src);
|
|
109
|
+
} catch {
|
|
110
|
+
// Error handled by videoError state
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
50
113
|
imageSrc.value = src;
|
|
51
|
-
// Reset state to ensure spinner shows
|
|
52
114
|
imageLoaded.value = false;
|
|
53
115
|
imageError.value = false;
|
|
54
116
|
try {
|
|
@@ -62,29 +124,56 @@ onMounted(async () => {
|
|
|
62
124
|
watch(
|
|
63
125
|
() => props.item?.src,
|
|
64
126
|
async (newSrc) => {
|
|
65
|
-
if (newSrc
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
127
|
+
if (!newSrc || showNotFound.value) return;
|
|
128
|
+
|
|
129
|
+
if (mediaType.value === 'video') {
|
|
130
|
+
if (newSrc !== videoSrc.value) {
|
|
131
|
+
videoLoaded.value = false;
|
|
132
|
+
videoError.value = false;
|
|
133
|
+
videoSrc.value = newSrc;
|
|
134
|
+
try {
|
|
135
|
+
await preloadVideo(newSrc);
|
|
136
|
+
} catch {
|
|
137
|
+
// Error handled by videoError state
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
if (newSrc !== imageSrc.value) {
|
|
142
|
+
imageLoaded.value = false;
|
|
143
|
+
imageError.value = false;
|
|
144
|
+
imageSrc.value = newSrc;
|
|
145
|
+
try {
|
|
146
|
+
await preloadImage(newSrc);
|
|
147
|
+
} catch {
|
|
148
|
+
// Error handled by imageError state
|
|
149
|
+
}
|
|
73
150
|
}
|
|
74
151
|
}
|
|
75
152
|
}
|
|
76
153
|
);
|
|
154
|
+
|
|
155
|
+
// mediaType and showNotFound are now computed, so they automatically react to changes
|
|
77
156
|
</script>
|
|
78
157
|
|
|
79
158
|
<template>
|
|
80
159
|
<div class="relative w-full h-full group">
|
|
81
160
|
<!-- Custom slot content (replaces default if provided) -->
|
|
82
|
-
<slot :item="item" :remove="remove" :imageLoaded="imageLoaded" :imageError="imageError">
|
|
161
|
+
<slot :item="item" :remove="remove" :imageLoaded="imageLoaded" :imageError="imageError" :videoLoaded="videoLoaded" :videoError="videoError" :showNotFound="showNotFound">
|
|
83
162
|
<!-- Default content when no slot is provided -->
|
|
84
163
|
<div class="w-full h-full rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 bg-white relative">
|
|
164
|
+
<!-- Not Found state -->
|
|
165
|
+
<div
|
|
166
|
+
v-if="showNotFound"
|
|
167
|
+
class="absolute inset-0 flex flex-col items-center justify-center bg-slate-100 text-slate-400 text-sm p-4 text-center"
|
|
168
|
+
>
|
|
169
|
+
<i class="fas fa-search text-3xl mb-3 opacity-50"></i>
|
|
170
|
+
<span class="font-medium">Not Found</span>
|
|
171
|
+
<span class="text-xs mt-1 opacity-75">This item could not be located</span>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
85
174
|
<!-- Spinner while loading -->
|
|
86
175
|
<div
|
|
87
|
-
v-if="!imageLoaded && !imageError"
|
|
176
|
+
v-else-if="(mediaType === 'image' && !imageLoaded && !imageError) || (mediaType === 'video' && !videoLoaded && !videoError)"
|
|
88
177
|
class="absolute inset-0 flex items-center justify-center bg-slate-100"
|
|
89
178
|
>
|
|
90
179
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
|
@@ -92,24 +181,37 @@ watch(
|
|
|
92
181
|
|
|
93
182
|
<!-- Error state -->
|
|
94
183
|
<div
|
|
95
|
-
v-if="imageError"
|
|
184
|
+
v-else-if="(mediaType === 'image' && imageError) || (mediaType === 'video' && videoError)"
|
|
96
185
|
class="absolute inset-0 flex flex-col items-center justify-center bg-slate-50 text-slate-400 text-sm p-4 text-center"
|
|
97
186
|
>
|
|
98
|
-
<i class="fas fa-image text-2xl mb-2 opacity-50"></i>
|
|
99
|
-
<span>Failed to load
|
|
187
|
+
<i :class="mediaType === 'video' ? 'fas fa-video text-2xl mb-2 opacity-50' : 'fas fa-image text-2xl mb-2 opacity-50'"></i>
|
|
188
|
+
<span>Failed to load {{ mediaType }}</span>
|
|
100
189
|
</div>
|
|
101
190
|
|
|
102
191
|
<!-- Image (only shown when loaded) -->
|
|
103
192
|
<img
|
|
104
|
-
v-if="imageLoaded && imageSrc"
|
|
193
|
+
v-if="mediaType === 'image' && imageLoaded && imageSrc && !showNotFound"
|
|
105
194
|
:src="imageSrc"
|
|
106
195
|
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
107
196
|
loading="lazy"
|
|
108
197
|
decoding="async"
|
|
109
198
|
/>
|
|
110
199
|
|
|
200
|
+
<!-- Video (only shown when loaded) -->
|
|
201
|
+
<video
|
|
202
|
+
v-if="mediaType === 'video' && videoLoaded && videoSrc && !showNotFound"
|
|
203
|
+
:src="videoSrc"
|
|
204
|
+
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
205
|
+
muted
|
|
206
|
+
loop
|
|
207
|
+
playsinline
|
|
208
|
+
@mouseenter="(e) => (e.target as HTMLVideoElement).play()"
|
|
209
|
+
@mouseleave="(e) => (e.target as HTMLVideoElement).pause()"
|
|
210
|
+
@error="videoError = true"
|
|
211
|
+
/>
|
|
212
|
+
|
|
111
213
|
<!-- 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>
|
|
214
|
+
<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 pointer-events-none"></div>
|
|
113
215
|
|
|
114
216
|
<!-- Remove button -->
|
|
115
217
|
<button
|
|
@@ -122,7 +224,7 @@ watch(
|
|
|
122
224
|
</button>
|
|
123
225
|
|
|
124
226
|
<!-- 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">
|
|
227
|
+
<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 pointer-events-none">
|
|
126
228
|
<p class="text-white text-xs font-medium truncate drop-shadow-md">Item #{{ String(item.id).split('-')[0] }}</p>
|
|
127
229
|
</div>
|
|
128
230
|
</div>
|