SEO, images, cleanup
@@ -2,11 +2,6 @@ import { globalIgnores } from 'eslint/config'
|
|||||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
import pluginVue from 'eslint-plugin-vue'
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
|
||||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
|
||||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
|
||||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
|
||||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
|
||||||
|
|
||||||
export default defineConfigWithVueTs(
|
export default defineConfigWithVueTs(
|
||||||
{
|
{
|
||||||
name: 'app/files-to-lint',
|
name: 'app/files-to-lint',
|
||||||
|
|||||||
91
index.html
@@ -1,19 +1,94 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="">
|
<html lang="en" style="background: #0b0b0b;">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<!-- Basic Meta Tags -->
|
||||||
<meta name="theme-color" content="#0b0b0b">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#0b0b0b" />
|
||||||
|
<meta name="author" content="David Haz">
|
||||||
|
<meta name="description"
|
||||||
|
content="An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces." />
|
||||||
|
<meta name="keywords"
|
||||||
|
content="Vue, Vue tutorials, Vue tips, Vue best practices, Vue development, Vue guides, Vue articles, Vue ecosystem, JavaScript, frontend development, VueJS, UI Library, Component Library, Vue Components, Vue Animations" />
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Font -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=Figtree:ital,wght@0,300..900;1,300..900&family=Gochi+Hand&display=swap"
|
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=Figtree:ital,wght@0,300..900;1,300..900&family=Figtree:wght@200..800&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Gochi+Hand&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<title>Vue Bits</title>
|
<!-- Icons -->
|
||||||
|
<link rel="icon" type="image/svg+xml" sizes="16x16 32x32" href="favicon.ico" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="shortcut icon" href="favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Vue Bits" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
|
||||||
|
<!-- Open Graph (OG) - Facebook, LinkedIn, etc. -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="Vue Bits">
|
||||||
|
<meta property="og:description"
|
||||||
|
content="An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces.">
|
||||||
|
<meta property="og:image" content="https://vuebits.dev/og-pic.jpg">
|
||||||
|
<meta property="og:image:alt" content="The Vue Bits landing page design, showcasing the logo and a subtitle!">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta property="og:url" content="https://vuebits.dev">
|
||||||
|
<meta property="og:site_name" content="Vue Bits">
|
||||||
|
<meta property="og:locale" content="en_US">
|
||||||
|
|
||||||
|
<!-- Twitter Card - Twitter Sharing -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Vue Bits">
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content="An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces.">
|
||||||
|
<meta name="twitter:image" content="https://vuebits.dev/og-pic.jpg">
|
||||||
|
<meta name="twitter:image:alt" content="The Vue Bits landing page design, showcasing the logo and a subtitle!">
|
||||||
|
|
||||||
|
<!-- Favicon & Apple Touch Icons -->
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
|
|
||||||
|
<!-- Canonical & Robots Meta Tags -->
|
||||||
|
<link rel="canonical" href="https://vuebits.dev">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
|
||||||
|
<!-- Structured Data (JSON-LD for SEO) -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"name": "Vue Bits",
|
||||||
|
"url": "https://vuebits.dev",
|
||||||
|
"description": "An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces.",
|
||||||
|
"image": "https://vuebits.dev/og-pic.jpg",
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "David Haz"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "David Haz",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://davidhaz.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<title>Vue Bits - Animated UI Components For Vue</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/og-pic.png
Normal file
|
After Width: | Height: | Size: 993 KiB |
1
public/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
43
scripts/generateComponent.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import process from "process";
|
||||||
|
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length < 2) {
|
||||||
|
console.error("Usage: npm run generate:component <ComponentType> <ComponentName>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [componentType, componentName] = args;
|
||||||
|
const componentNameLower = componentName.charAt(0).toLowerCase() + componentName.slice(1);
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
content: path.join(__dirname, "../src/content", componentType, componentName),
|
||||||
|
demo: path.join(__dirname, "../src/demo", componentType),
|
||||||
|
constants: path.join(__dirname, "../src/constants/code", componentType),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.values(paths).forEach((dir) => {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
path.join(paths.content, `${componentName}.vue`),
|
||||||
|
path.join(paths.demo, `${componentName}Demo.jsx`),
|
||||||
|
path.join(paths.constants, `${componentNameLower}Code.js`),
|
||||||
|
];
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
fs.writeFileSync(file, "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Component "${componentName}" structure created successfully under "${componentType}".`);
|
||||||
@@ -117,17 +117,13 @@ void mainImage(out vec4 C, in vec2 U) {
|
|||||||
|
|
||||||
vec3 c = max(cos(d * pi2) - s * sqrt(d) - k, 0.0);
|
vec3 c = max(cos(d * pi2) - s * sqrt(d) - k, 0.0);
|
||||||
|
|
||||||
// Vue.js colors: #42B883 (green) and #35495e (dark gray)
|
|
||||||
vec3 vueGreen = vec3(0.259, 0.722, 0.514); // #42B883
|
vec3 vueGreen = vec3(0.259, 0.722, 0.514); // #42B883
|
||||||
vec3 vueDark = vec3(0.208, 0.286, 0.369); // #35495e
|
vec3 vueDark = vec3(0.208, 0.286, 0.369); // #35495e
|
||||||
|
|
||||||
// Use different colors for different wave components
|
|
||||||
vec3 finalColor = vec3(0.0);
|
vec3 finalColor = vec3(0.0);
|
||||||
if (k.x < k.y) {
|
if (k.x < k.y) {
|
||||||
// First wave component - Vue green
|
|
||||||
finalColor = vueGreen * c.x;
|
finalColor = vueGreen * c.x;
|
||||||
} else {
|
} else {
|
||||||
// Second wave component - Vue dark gray
|
|
||||||
finalColor = vueDark * c.y;
|
finalColor = vueDark * c.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ const stars = useStars()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// Helper function
|
|
||||||
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
|
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
|
||||||
|
|
||||||
const toggleDrawer = () => {
|
const toggleDrawer = () => {
|
||||||
@@ -134,7 +133,6 @@ const handleKeyDown = (e: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category Component
|
|
||||||
const Category = defineComponent({
|
const Category = defineComponent({
|
||||||
name: 'Category',
|
name: 'Category',
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ import '../../css/sidebar.css'
|
|||||||
|
|
||||||
const HOVER_TIMEOUT_DELAY = 150
|
const HOVER_TIMEOUT_DELAY = 150
|
||||||
|
|
||||||
// Reactive data
|
|
||||||
const isDrawerOpen = ref(false)
|
const isDrawerOpen = ref(false)
|
||||||
const linePosition = ref<number | null>(null)
|
const linePosition = ref<number | null>(null)
|
||||||
const isLineVisible = ref(false)
|
const isLineVisible = ref(false)
|
||||||
@@ -95,14 +94,12 @@ const isTransitioning = ref(false)
|
|||||||
const sidebarRef = ref<HTMLDivElement>()
|
const sidebarRef = ref<HTMLDivElement>()
|
||||||
const sidebarContainerRef = ref<HTMLDivElement>()
|
const sidebarContainerRef = ref<HTMLDivElement>()
|
||||||
|
|
||||||
// Timeouts
|
|
||||||
let hoverTimeoutRef: number | null = null
|
let hoverTimeoutRef: number | null = null
|
||||||
let hoverDelayTimeoutRef: number | null = null
|
let hoverDelayTimeoutRef: number | null = null
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
const scrollToTop = () => window.scrollTo(0, 0)
|
const scrollToTop = () => window.scrollTo(0, 0)
|
||||||
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
|
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
|
||||||
|
|
||||||
@@ -115,7 +112,6 @@ const findActiveElement = () => {
|
|||||||
return activePath === expectedPath
|
return activePath === expectedPath
|
||||||
})
|
})
|
||||||
if (activeItem) {
|
if (activeItem) {
|
||||||
// Try to find the element within the sidebar
|
|
||||||
const selector = `.sidebar a[href="${activePath}"]`
|
const selector = `.sidebar a[href="${activePath}"]`
|
||||||
const element = document.querySelector(selector) as HTMLElement
|
const element = document.querySelector(selector) as HTMLElement
|
||||||
return element
|
return element
|
||||||
@@ -145,7 +141,6 @@ const handleTransitionNavigation = async (path: string) => {
|
|||||||
|
|
||||||
pendingActivePath.value = path
|
pendingActivePath.value = path
|
||||||
|
|
||||||
// Simple navigation without transition for now
|
|
||||||
// TODO: Implement transition when available
|
// TODO: Implement transition when available
|
||||||
await router.push(path)
|
await router.push(path)
|
||||||
scrollToTop()
|
scrollToTop()
|
||||||
@@ -158,7 +153,6 @@ const handleMobileTransitionNavigation = async (path: string) => {
|
|||||||
closeDrawer()
|
closeDrawer()
|
||||||
pendingActivePath.value = path
|
pendingActivePath.value = path
|
||||||
|
|
||||||
// Simple navigation without transition for now
|
|
||||||
// TODO: Implement transition when available
|
// TODO: Implement transition when available
|
||||||
await router.push(path)
|
await router.push(path)
|
||||||
scrollToTop()
|
scrollToTop()
|
||||||
@@ -201,7 +195,6 @@ const handleScroll = () => {
|
|||||||
const updateActiveLine = async () => {
|
const updateActiveLine = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
// Wait a bit more to ensure DOM is fully updated
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const activeEl = findActiveElement()
|
const activeEl = findActiveElement()
|
||||||
|
|
||||||
@@ -220,7 +213,6 @@ const updateActiveLine = async () => {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category Component
|
|
||||||
const Category = defineComponent({
|
const Category = defineComponent({
|
||||||
name: 'Category',
|
name: 'Category',
|
||||||
props: {
|
props: {
|
||||||
@@ -311,11 +303,9 @@ const Category = defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watchers
|
|
||||||
watch(() => route.path, updateActiveLine)
|
watch(() => route.path, updateActiveLine)
|
||||||
watch(pendingActivePath, updateActiveLine)
|
watch(pendingActivePath, updateActiveLine)
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateActiveLine()
|
updateActiveLine()
|
||||||
if (sidebarContainerRef.value) {
|
if (sidebarContainerRef.value) {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export function useStars() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching stars:', error)
|
console.error('Error fetching stars:', error)
|
||||||
|
|
||||||
// Fall back to cached data if available
|
|
||||||
const cachedData = localStorage.getItem(CACHE_KEY)
|
const cachedData = localStorage.getItem(CACHE_KEY)
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
const { count } = JSON.parse(cachedData)
|
const { count } = JSON.parse(cachedData)
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ const splitterRef = ref<GSAPSplitText | null>(null)
|
|||||||
const initializeAnimation = async () => {
|
const initializeAnimation = async () => {
|
||||||
if (typeof window === 'undefined' || !textRef.value || !props.text) return
|
if (typeof window === 'undefined' || !textRef.value || !props.text) return
|
||||||
|
|
||||||
// Wait for DOM to be fully updated
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
const el = textRef.value
|
const el = textRef.value
|
||||||
@@ -174,7 +173,6 @@ onUnmounted(() => {
|
|||||||
cleanup()
|
cleanup()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for prop changes and reinitialize animation
|
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
() => props.text,
|
() => props.text,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
// PrimeVue imports
|
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
import Aura from '@primeuix/themes/aura'
|
import Aura from '@primeuix/themes/aura'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
@@ -26,7 +25,6 @@ app.use(PrimeVue, {
|
|||||||
})
|
})
|
||||||
app.use(ToastService)
|
app.use(ToastService)
|
||||||
|
|
||||||
// Global components
|
|
||||||
app.component('Button', Button)
|
app.component('Button', Button)
|
||||||
app.component('Toast', Toast)
|
app.component('Toast', Toast)
|
||||||
app.component('Select', Select)
|
app.component('Select', Select)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<component :is="SubcategoryComponent" v-if="SubcategoryComponent" />
|
<component :is="SubcategoryComponent" v-if="SubcategoryComponent" />
|
||||||
</template>
|
</template>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="loading-placeholder">Loading...</div>
|
<div class="loading-placeholder"></div>
|
||||||
</template>
|
</template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,54 +17,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, nextTick, defineAsyncComponent } from 'vue'
|
import { ref, computed, watch, onMounted, nextTick, defineAsyncComponent } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { componentMap } from '../constants/Components'
|
import { componentMap } from '../constants/Components'
|
||||||
import { decodeLabel } from '../utils/utils'
|
import { decodeLabel } from '../utils/utils'
|
||||||
import BackToTopButton from '@/components/common/BackToTopButton.vue'
|
import BackToTopButton from '@/components/common/BackToTopButton.vue'
|
||||||
import '../css/category.css'
|
import '../css/category.css'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const scrollRef = ref<HTMLDivElement | null>(null)
|
const scrollRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const subcategory = computed(() => route.params.subcategory as string)
|
const subcategory = computed(() => route.params.subcategory as string)
|
||||||
const decodedLabel = computed(() => decodeLabel(subcategory.value))
|
const decodedLabel = computed(() => decodeLabel(subcategory.value))
|
||||||
|
|
||||||
// Lazy load the component based on subcategory
|
const SubcategoryComponent = computed(() => {
|
||||||
const SubcategoryComponent = computed(() => {
|
if (!subcategory.value) {
|
||||||
if (!subcategory.value) {
|
return null
|
||||||
return null
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const componentLoader = componentMap[subcategory.value as keyof typeof componentMap]
|
const componentLoader = componentMap[subcategory.value as keyof typeof componentMap]
|
||||||
|
|
||||||
if (!componentLoader) {
|
if (!componentLoader) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return defineAsyncComponent(componentLoader)
|
return defineAsyncComponent(componentLoader)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update document title
|
watch(decodedLabel, (newLabel) => {
|
||||||
watch(decodedLabel, (newLabel) => {
|
if (newLabel) {
|
||||||
if (newLabel) {
|
document.title = `Vue Bits - ${newLabel}`
|
||||||
document.title = `Vue Bits - ${newLabel}`
|
}
|
||||||
}
|
}, { immediate: true })
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// Scroll to top when subcategory changes
|
watch(subcategory, async () => {
|
||||||
watch(subcategory, async () => {
|
if (scrollRef.value) {
|
||||||
if (scrollRef.value) {
|
await nextTick()
|
||||||
await nextTick()
|
scrollRef.value.scrollTo(0, 0)
|
||||||
scrollRef.value.scrollTo(0, 0)
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Initial scroll to top
|
if (scrollRef.value) {
|
||||||
if (scrollRef.value) {
|
scrollRef.value.scrollTo(0, 0)
|
||||||
scrollRef.value.scrollTo(0, 0)
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||