Add prettier config, format codebase

This commit is contained in:
David Haz
2025-07-12 11:59:33 +03:00
parent ac8b2c04d8
commit f4d97ee94e
211 changed files with 10586 additions and 8810 deletions

View File

@@ -1,7 +1,7 @@
name: 🐞 Bug report
description: Help improve Vue Bits.
labels: ["bug"]
title: "[BUG]: "
labels: ['bug']
title: '[BUG]: '
body:
- type: markdown
attributes:

View File

@@ -1,7 +1,7 @@
name: 💡 Feature Request
description: Suggest something for Vue Bits.
labels: ["enhancement"]
title: "[FEAT]: "
labels: ['enhancement']
title: '[FEAT]: '
body:
- type: markdown
attributes:

34
.prettierignore Normal file
View File

@@ -0,0 +1,34 @@
# Dependencies
node_modules/
# Build outputs
dist/
build/
public/ui/
# OS files
.DS_Store
Thumbs.db
# IDE files
.vscode/
.idea/
# Logs
*.log
# Cache
.cache/
.parcel-cache/
.nuxt/
.next/
.vite/
# Environment files
.env
.env.local
.env.*.local
# Generated files
coverage/
*.tsbuildinfo

14
.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 120,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"vueIndentScriptAndStyle": false,
"htmlWhitespaceSensitivity": "ignore",
"bracketSameLine": false,
"singleAttributePerLine": false
}

View File

@@ -62,7 +62,7 @@ Please review the [Contribution Guide](https://github.com/DavidHDev/vue-bits/blo
## Stats
![Alt](https://repobeats.axiom.co/api/embed/02689c621a09cc5b492ccc1b4bb2f764e32500b7.svg "Repobeats analytics image")
![Alt](https://repobeats.axiom.co/api/embed/02689c621a09cc5b492ccc1b4bb2f764e32500b7.svg 'Repobeats analytics image')
## Sponsorship

View File

@@ -1,11 +1,11 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import { globalIgnores } from 'eslint/config';
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
import pluginVue from 'eslint-plugin-vue';
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
files: ['**/*.{ts,mts,tsx,vue}']
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
@@ -18,7 +18,7 @@ export default defineConfigWithVueTs(
files: ['**/*.vue'],
rules: {
'vue/multi-word-component-names': 'off',
'vue/no-reserved-component-names': 'off',
},
},
)
'vue/no-reserved-component-names': 'off'
}
}
);

View File

@@ -1,31 +1,36 @@
<!doctype html>
<html lang="en" style="background: #0b0b0b;">
<html lang="en" style="background: #0b0b0b">
<head>
<!-- Basic Meta Tags -->
<meta charset="UTF-8" />
<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" />
<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"
/>
<!-- Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
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">
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" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" sizes="16x16 32x32" href="favicon.ico" />
@@ -36,34 +41,38 @@
<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://vue-bits.dev/og-pic.png">
<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://vue-bits.dev">
<meta property="og:site_name" content="Vue Bits">
<meta property="og:locale" content="en_US">
<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://vue-bits.dev/og-pic.png" />
<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://vue-bits.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://vue-bits.dev/og-pic.jpg">
<meta name="twitter:image:alt" content="The Vue Bits landing page design, showcasing the logo and a subtitle!">
<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://vue-bits.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">
<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://vue-bits.dev">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://vue-bits.dev" />
<meta name="robots" content="index, follow" />
<!-- Structured Data (JSON-LD for SEO) -->
<script type="application/ld+json">
@@ -95,5 +104,4 @@
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1
package-lock.json generated
View File

@@ -41,6 +41,7 @@
"jsrepo": "^1.30.1",
"npm-run-all2": "^8.0.4",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.11",
"typescript": "~5.8.0",
"vite": "^7.0.0",

View File

@@ -11,6 +11,8 @@
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"new:component": "node scripts/generateComponent.js"
},
"dependencies": {
@@ -47,6 +49,7 @@
"jsrepo": "^1.30.1",
"npm-run-all2": "^8.0.4",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.11",
"typescript": "~5.8.0",
"vite": "^7.0.0",

View File

@@ -1 +1,11 @@
{"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"}
{
"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"
}

View File

View File

@@ -1,15 +1,15 @@
import fs from "fs";
import path from "path";
import process from "process";
import fs from 'fs';
import path from 'path';
import process from 'process';
import { fileURLToPath } from "url";
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>");
console.error('Usage: npm run generate:component <ComponentType> <ComponentName>');
process.exit(1);
}
@@ -17,12 +17,12 @@ 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),
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) => {
Object.values(paths).forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
@@ -31,12 +31,12 @@ Object.values(paths).forEach((dir) => {
const files = [
path.join(paths.content, `${componentName}.vue`),
path.join(paths.demo, `${componentName}Demo.vue`),
path.join(paths.constants, `${componentNameLower}Code.ts`),
path.join(paths.constants, `${componentNameLower}Code.ts`)
];
files.forEach((file) => {
files.forEach(file => {
if (!fs.existsSync(file)) {
fs.writeFileSync(file, "");
fs.writeFileSync(file, '');
}
});

View File

@@ -1,27 +1,24 @@
<template>
<div>
<DisplayHeader
v-if="!isCategoryPage"
:activeItem="activeItem"
/>
<DisplayHeader v-if="!isCategoryPage" :activeItem="activeItem" />
<router-view />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import DisplayHeader from '@/components/landing/DisplayHeader/DisplayHeader.vue'
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import DisplayHeader from '@/components/landing/DisplayHeader/DisplayHeader.vue';
const route = useRoute()
const route = useRoute();
const activeItem = computed(() => {
if (route.path === '/') return 'home'
return null
})
if (route.path === '/') return 'home';
return null;
});
const isCategoryPage = computed(() => {
return /^\/[^/]+\/[^/]+$/.test(route.path)
})
return /^\/[^/]+\/[^/]+$/.test(route.path);
});
</script>

View File

@@ -2,12 +2,21 @@
<div class="cli-installation">
<h2 class="demo-title">One-Time Installation</h2>
<VCodeBlock v-if="command" :code="command" :persistent-copy-button="true" highlightjs lang="bash" theme="nord"
:copy-button="true" class="code-block" />
<VCodeBlock
v-if="command"
:code="command"
:persistent-copy-button="true"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
<div class="cli-divider"></div>
<h2 class="demo-title">Full CLI Setup</h2>
<p class="jsrepo-info">
Vue Bits uses
<a href="https://jsrepo.dev/" target="_blank" rel="noreferrer">jsrepo</a>
@@ -17,23 +26,57 @@
<Accordion expandIcon="pi pi-chevron-right" collapseIcon="pi pi-chevron-down">
<AccordionPanel value="setup">
<AccordionHeader>Setup Steps</AccordionHeader>
<AccordionContent
:pt="{ transition: { enterFromClass: '', enterActiveClass: '', enterToClass: '', leaveFromClass: '', leaveActiveClass: '', leaveToClass: '' } }">
:pt="{
transition: {
enterFromClass: '',
enterActiveClass: '',
enterToClass: '',
leaveFromClass: '',
leaveActiveClass: '',
leaveToClass: ''
}
}"
>
<div class="setup-content">
<p class="demo-extra-info">1. Initialize a config file for your project</p>
<div class="setup-option">
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo init https://vue-bits.dev/ui" highlightjs
lang="bash" theme="nord" :copy-button="true" class="code-block" />
<VCodeBlock
:persistent-copy-button="true"
code="npx jsrepo init https://vue-bits.dev/ui"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
</div>
<p class="demo-extra-info">2. Browse &amp; add components from the list</p>
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo add" highlightjs lang="bash" theme="nord"
:copy-button="true" class="code-block" />
<VCodeBlock
:persistent-copy-button="true"
code="npx jsrepo add"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
<p class="demo-extra-info">3. Or just add a specific component</p>
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo add Animations/AnimatedContainer" highlightjs
lang="bash" theme="nord" :copy-button="true" class="code-block" />
<VCodeBlock
:persistent-copy-button="true"
code="npx jsrepo add Animations/AnimatedContainer"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
</div>
</AccordionContent>
</AccordionPanel>
@@ -42,15 +85,15 @@
</template>
<script setup lang="ts">
import { VCodeBlock } from '@wdns/vue-code-block'
import Accordion from 'primevue/accordion'
import AccordionPanel from 'primevue/accordionpanel'
import AccordionHeader from 'primevue/accordionheader'
import AccordionContent from 'primevue/accordioncontent'
import { VCodeBlock } from '@wdns/vue-code-block';
import Accordion from 'primevue/accordion';
import AccordionPanel from 'primevue/accordionpanel';
import AccordionHeader from 'primevue/accordionheader';
import AccordionContent from 'primevue/accordioncontent';
const { command } = defineProps<{
command?: string
}>()
command?: string;
}>();
</script>
<style scoped>
@@ -74,7 +117,7 @@ const { command } = defineProps<{
}
.jsrepo-info a {
color: #27FF64;
color: #27ff64;
text-decoration: underline;
}

View File

@@ -4,14 +4,25 @@
<h2 class="demo-title">{{ getDisplayName(name) }}</h2>
<div v-if="snippet" class="code-container">
<div class="code-wrapper" :class="{ 'collapsed': shouldCollapse(snippet) && !isExpanded(name) }">
<VCodeBlock :code="snippet" highlightjs :lang="getLanguage(name)" theme="nord" :copy-button="true"
:persistent-copy-button="true" class="code-block" />
<div class="code-wrapper" :class="{ collapsed: shouldCollapse(snippet) && !isExpanded(name) }">
<VCodeBlock
:code="snippet"
highlightjs
:lang="getLanguage(name)"
theme="nord"
:copy-button="true"
:persistent-copy-button="true"
class="code-block"
/>
<div v-if="shouldCollapse(snippet) && !isExpanded(name)" class="fade-overlay" />
<button v-if="shouldCollapse(snippet)" class="expand-button" :class="{ 'expanded': isExpanded(name) }"
@click="toggleExpanded(name)">
<button
v-if="shouldCollapse(snippet)"
class="expand-button"
:class="{ expanded: isExpanded(name) }"
@click="toggleExpanded(name)"
>
{{ isExpanded(name) ? 'Collapse Snippet' : 'See Full Snippet' }}
</button>
</div>
@@ -19,6 +30,7 @@
<div v-if="!snippet" class="no-code">
<span>Nothing here yet!</span>
<i class="pi pi-face-sad"></i>
</div>
</div>
@@ -26,58 +38,55 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VCodeBlock } from '@wdns/vue-code-block'
import type { CodeObject } from '../../types/code'
import { computed, ref } from 'vue';
import { VCodeBlock } from '@wdns/vue-code-block';
import type { CodeObject } from '../../types/code';
const props = defineProps<{
codeObject: CodeObject
}>()
codeObject: CodeObject;
}>();
const skipKeys = [
'cli'
]
const skipKeys = ['cli'];
const expandedSections = ref<Set<string>>(new Set())
const expandedSections = ref<Set<string>>(new Set());
const codeEntries = computed(() => {
return Object.entries(props.codeObject).filter(([name]) => !skipKeys.includes(name))
})
return Object.entries(props.codeObject).filter(([name]) => !skipKeys.includes(name));
});
const shouldCollapse = (snippet: string) => {
const codeLines = snippet?.split('\n').length || 0
return codeLines > 35
}
const codeLines = snippet?.split('\n').length || 0;
return codeLines > 35;
};
const isExpanded = (name: string) => {
return expandedSections.value.has(name)
}
return expandedSections.value.has(name);
};
const toggleExpanded = (name: string) => {
if (expandedSections.value.has(name)) {
expandedSections.value.delete(name)
expandedSections.value.delete(name);
} else {
expandedSections.value.add(name)
}
expandedSections.value.add(name);
}
};
const getDisplayName = (name: string) => {
if (name === 'code') return 'Code'
if (name === 'cli') return 'CLI Command'
if (name === 'utility') return 'Utility'
if (name === 'usage') return 'Usage'
if (name === 'installation') return 'Installation'
return name.charAt(0).toUpperCase() + name.slice(1)
}
if (name === 'code') return 'Code';
if (name === 'cli') return 'CLI Command';
if (name === 'utility') return 'Utility';
if (name === 'usage') return 'Usage';
if (name === 'installation') return 'Installation';
return name.charAt(0).toUpperCase() + name.slice(1);
};
const getLanguage = (name: string) => {
if (name === 'cli') return 'bash'
if (name === 'code') return 'html'
if (name === 'usage') return 'html'
if (name === 'installation') return 'bash'
return 'javascript'
}
if (name === 'cli') return 'bash';
if (name === 'code') return 'html';
if (name === 'usage') return 'html';
if (name === 'installation') return 'bash';
return 'javascript';
};
</script>
<style scoped>

View File

@@ -1,18 +1,17 @@
<template>
<div class="dependencies-container">
<h2 class="demo-title">Dependencies</h2>
<div class="demo-details">
<span v-for="dep in dependencyList" :key="dep" class="dependency-tag">
{{ dep }}
</span>
<span v-for="dep in dependencyList" :key="dep" class="dependency-tag">{{ dep }}</span>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
dependencyList: string[]
dependencyList: string[];
}
defineProps<Props>()
defineProps<Props>();
</script>

View File

@@ -1,5 +1,6 @@
<template>
<Button :style="{
<Button
:style="{
fontWeight: 500,
borderRadius: '0.75rem',
border: '1px solid #142216',
@@ -12,18 +13,21 @@
opacity: visible ? 1 : 0,
bottom: visible ? '2.5em' : '1em',
cursor: visible ? 'pointer' : 'default'
}" class="back-to-top" @click="visible && scrollToTop()">
<i class="pi pi-arrow-up" style="color: #fff; font-size: 1rem;"></i>
}"
class="back-to-top"
@click="visible && scrollToTop()"
>
<i class="pi pi-arrow-up" style="color: #fff; font-size: 1rem"></i>
</Button>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useToast } from 'primevue/usetoast'
import Button from 'primevue/button'
import { ref, onMounted, onUnmounted } from 'vue';
import { useToast } from 'primevue/usetoast';
import Button from 'primevue/button';
const toast = useToast()
const visible = ref(false)
const toast = useToast();
const visible = ref(false);
const messages = [
'🐴 Country roads, take me home!',
@@ -33,34 +37,33 @@ const messages = [
'🐉 Fus Ro Dah!',
'🍄 The princess is in another castle!',
'🦸‍♂️ Avengers, assemble!',
'🗡️ It\'s dangerous to go alone! Take this.',
"🗡️ It's dangerous to go alone! Take this.",
'📜 A wizard is never late.',
'💍 Foul Tarnished, in search of the Elden Ring!',
'🐊 See you later, alligator.',
'🔥 Dracarys!'
]
];
const getRandomMessage = (messages: string[]) =>
messages[Math.floor(Math.random() * messages.length)]
const getRandomMessage = (messages: string[]) => messages[Math.floor(Math.random() * messages.length)];
const scrollToTop = () => {
window.scrollTo(0, 0)
window.scrollTo(0, 0);
toast.add({
severity: 'secondary',
summary: getRandomMessage(messages),
life: 3000
})
}
});
};
const onScroll = () => {
visible.value = window.scrollY > 500
}
visible.value = window.scrollY > 500;
};
onMounted(() => {
window.addEventListener('scroll', onScroll)
})
window.addEventListener('scroll', onScroll);
});
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
window.removeEventListener('scroll', onScroll);
});
</script>

View File

@@ -1,24 +1,19 @@
<template>
<div class="contribute-container">
<h2 class="demo-title-contribute">Help improve this component!</h2>
<div class="contribute-buttons">
<a
:href="bugReportUrl"
target="_blank"
rel="noreferrer"
class="contribute-button"
>
<a :href="bugReportUrl" target="_blank" rel="noreferrer" class="contribute-button">
<i class="pi pi-exclamation-triangle"></i>
<span>Report an issue</span>
</a>
<span class="contribute-separator">or</span>
<a
:href="featureRequestUrl"
target="_blank"
rel="noreferrer"
class="contribute-button"
>
<a :href="featureRequestUrl" target="_blank" rel="noreferrer" class="contribute-button">
<i class="pi pi-lightbulb"></i>
<span>Request a feature</span>
</a>
</div>
@@ -26,22 +21,22 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const route = useRoute();
const bugReportUrl = computed(() => {
const category = route.params.category
const subcategory = route.params.subcategory
const title = encodeURIComponent(`[BUG]: ${category}/${subcategory}`)
return `https://github.com/DavidHDev/vue-bits/issues/new?template=1-bug-report.yml&title=${title}&labels=bug`
})
const category = route.params.category;
const subcategory = route.params.subcategory;
const title = encodeURIComponent(`[BUG]: ${category}/${subcategory}`);
return `https://github.com/DavidHDev/vue-bits/issues/new?template=1-bug-report.yml&title=${title}&labels=bug`;
});
const featureRequestUrl = computed(() => {
const category = route.params.category
const subcategory = route.params.subcategory
const title = encodeURIComponent(`[FEAT]: ${category}/${subcategory}`)
return `https://github.com/DavidHDev/vue-bits/issues/new?template=2-feature-request.yml&title=${title}&labels=enhancement`
})
const category = route.params.category;
const subcategory = route.params.subcategory;
const title = encodeURIComponent(`[FEAT]: ${category}/${subcategory}`);
return `https://github.com/DavidHDev/vue-bits/issues/new?template=2-feature-request.yml&title=${title}&labels=enhancement`;
});
</script>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<h2 class="demo-title">Customize</h2>
<slot />
</div>
</template>

View File

@@ -1,28 +1,45 @@
<template>
<svg width="141" height="30" viewBox="0 0 193 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M66.4663 34.2676L56.3843 7.12372H60.5722L68.7929 30.2348L77.0912 7.12372H81.2015L71.1195 34.2676H66.4663Z"
fill="white" />
<path
d="M66.4663 34.2676L56.3843 7.12372H60.5722L68.7929 30.2348L77.0912 7.12372H81.2015L71.1195 34.2676H66.4663Z"
fill="white"
/>
<path
d="M88.3915 34.7329C86.8662 34.7329 85.5349 34.4227 84.3974 33.8023C83.2858 33.1818 82.4198 32.2512 81.7994 31.0103C81.2048 29.7695 80.9075 28.2055 80.9075 26.3183V14.724H84.7852V25.8918C84.7852 27.7272 85.1859 29.1103 85.9873 30.0409C86.7887 30.9716 87.9391 31.4369 89.4384 31.4369C90.4466 31.4369 91.3514 31.1913 92.1528 30.7001C92.9801 30.2089 93.6264 29.498 94.0917 28.5674C94.557 27.6367 94.7897 26.4993 94.7897 25.155V14.724H98.6674V34.2676H95.2162L94.9448 30.9328C94.3502 32.1219 93.4842 33.0526 92.3467 33.7247C91.2092 34.3969 89.8908 34.7329 88.3915 34.7329Z"
fill="white" />
fill="white"
/>
<path
d="M110.648 34.7329C108.787 34.7329 107.133 34.3064 105.685 33.4533C104.237 32.6002 103.1 31.411 102.273 29.8858C101.471 28.3606 101.071 26.5898 101.071 24.5734C101.071 22.5053 101.471 20.7086 102.273 19.1834C103.1 17.6323 104.237 16.4302 105.685 15.5771C107.133 14.6982 108.813 14.2587 110.726 14.2587C112.639 14.2587 114.281 14.6852 115.651 15.5383C117.021 16.3914 118.081 17.5289 118.83 18.9507C119.58 20.3467 119.955 21.8977 119.955 23.6039C119.955 23.8624 119.942 24.1468 119.916 24.457C119.916 24.7414 119.903 25.0645 119.877 25.4265H103.901V22.6733H116.077C116 21.0447 115.457 19.7779 114.449 18.8731C113.44 17.9425 112.187 17.4772 110.687 17.4772C109.627 17.4772 108.658 17.7228 107.779 18.2139C106.9 18.6793 106.189 19.3772 105.646 20.3079C105.129 21.2127 104.871 22.3631 104.871 23.759V24.8448C104.871 26.2925 105.129 27.5204 105.646 28.5286C106.189 29.511 106.9 30.2606 107.779 30.7777C108.658 31.2689 109.614 31.5144 110.648 31.5144C111.889 31.5144 112.91 31.243 113.712 30.7001C114.513 30.1572 115.108 29.4205 115.496 28.4898H119.373C119.037 29.679 118.468 30.7518 117.667 31.7083C116.866 32.639 115.87 33.3757 114.681 33.9186C113.518 34.4615 112.174 34.7329 110.648 34.7329Z"
fill="white" />
fill="white"
/>
<path
d="M130.615 34.2676V7.12372H140.658C142.545 7.12372 144.122 7.43394 145.389 8.05437C146.656 8.64895 147.599 9.47619 148.22 10.5361C148.866 11.5701 149.189 12.7464 149.189 14.0648C149.189 15.4349 148.892 16.5853 148.297 17.5159C147.703 18.4466 146.914 19.1704 145.932 19.6875C144.975 20.1786 143.941 20.463 142.83 20.5406L143.373 20.1528C144.562 20.1786 145.648 20.5018 146.63 21.1222C147.612 21.7168 148.388 22.5182 148.956 23.5264C149.525 24.5346 149.81 25.6462 149.81 26.8612C149.81 28.2572 149.474 29.5239 148.801 30.6613C148.129 31.773 147.134 32.6519 145.816 33.2982C144.497 33.9445 142.881 34.2676 140.968 34.2676H130.615ZM134.493 31.0491H140.464C142.171 31.0491 143.489 30.6613 144.42 29.8858C145.376 29.0844 145.854 27.9599 145.854 26.5122C145.854 25.0904 145.363 23.9529 144.381 23.0998C143.424 22.2467 142.093 21.8202 140.387 21.8202H134.493V31.0491ZM134.493 18.8344H140.232C141.86 18.8344 143.101 18.4595 143.954 17.7098C144.807 16.9343 145.234 15.8744 145.234 14.5301C145.234 13.2376 144.807 12.2164 143.954 11.4667C143.101 10.6912 141.822 10.3034 140.115 10.3034H134.493V18.8344Z"
fill="white" />
fill="white"
/>
<path
d="M153.736 34.2676V14.724H157.614V34.2676H153.736ZM155.714 11.0402C154.964 11.0402 154.344 10.8075 153.852 10.3422C153.387 9.87689 153.154 9.2823 153.154 8.55847C153.154 7.86048 153.387 7.29175 153.852 6.85228C154.344 6.38696 154.964 6.1543 155.714 6.1543C156.438 6.1543 157.045 6.38696 157.536 6.85228C158.027 7.29175 158.273 7.86048 158.273 8.55847C158.273 9.2823 158.027 9.87689 157.536 10.3422C157.045 10.8075 156.438 11.0402 155.714 11.0402Z"
fill="white" />
fill="white"
/>
<path
d="M170.449 34.2676C169.208 34.2676 168.135 34.0737 167.23 33.6859C166.326 33.2982 165.628 32.6519 165.136 31.7471C164.645 30.8423 164.4 29.6144 164.4 28.0633V18.02H161.026V14.724H164.4L164.865 9.83811H168.277V14.724H173.823V18.02H168.277V28.1021C168.277 29.2137 168.51 29.9763 168.975 30.3899C169.441 30.7777 170.242 30.9716 171.38 30.9716H173.629V34.2676H170.449Z"
fill="white" />
fill="white"
/>
<path
d="M184.885 34.7329C183.23 34.7329 181.782 34.4615 180.542 33.9186C179.301 33.3757 178.318 32.6131 177.595 31.6308C176.871 30.6484 176.431 29.498 176.276 28.1796H180.231C180.361 28.8 180.606 29.3688 180.968 29.8858C181.356 30.4028 181.873 30.8165 182.519 31.1267C183.191 31.4369 183.98 31.592 184.885 31.592C185.738 31.592 186.436 31.4757 186.979 31.243C187.547 30.9845 187.961 30.6484 188.219 30.2348C188.478 29.7953 188.607 29.33 188.607 28.8388C188.607 28.115 188.426 27.5721 188.064 27.2102C187.728 26.8224 187.211 26.5251 186.513 26.3183C185.841 26.0857 185.027 25.8789 184.07 25.6979C183.165 25.5428 182.287 25.336 181.433 25.0775C180.606 24.7931 179.856 24.4441 179.184 24.0305C178.538 23.6169 178.021 23.0998 177.633 22.4794C177.246 21.8331 177.052 21.0447 177.052 20.114C177.052 19.0024 177.349 18.0071 177.943 17.1282C178.538 16.2234 179.378 15.5254 180.464 15.0342C181.576 14.5172 182.881 14.2587 184.38 14.2587C186.552 14.2587 188.297 14.7757 189.615 15.8098C190.934 16.8438 191.709 18.3044 191.942 20.1916H188.181C188.077 19.3126 187.689 18.6405 187.017 18.1752C186.345 17.684 185.453 17.4384 184.342 17.4384C183.23 17.4384 182.377 17.6581 181.782 18.0976C181.188 18.5371 180.891 19.1187 180.891 19.8426C180.891 20.3079 181.059 20.7215 181.395 21.0834C181.731 21.4453 182.222 21.7556 182.868 22.0141C183.54 22.2467 184.355 22.4665 185.311 22.6733C186.681 22.9318 187.909 23.2549 188.995 23.6427C190.081 24.0305 190.947 24.5992 191.593 25.3489C192.239 26.0986 192.562 27.1714 192.562 28.5674C192.588 29.7824 192.278 30.8552 191.632 31.7859C191.011 32.7165 190.119 33.4404 188.956 33.9574C187.819 34.4744 186.462 34.7329 184.885 34.7329Z"
fill="white" />
fill="white"
/>
<path
d="M0.345215 0.450195H15.6363C15.6363 0.450195 22.0583 12.3486 26.6885 19.6614C26.8836 19.9694 26.9797 20.151 27.1894 20.4493C27.3852 20.7278 27.4954 20.8853 27.7214 21.1399C27.9339 21.3795 27.8963 21.356 28.1397 21.6249C28.2726 21.7719 28.3875 21.8829 28.6738 22.068C28.933 22.2355 29.377 22.3481 29.7138 22.2345C29.9746 22.1465 30.0451 22.1136 30.2668 21.9894C31.043 21.5543 31.8402 19.9698 31.8402 19.9698L40.7527 3.71523H32.9372V0.450859L46.3206 0.450195L35.594 19.0675C35.594 19.0675 34.5027 20.8176 33.7209 22.1763C33.5935 22.3977 33.0493 23.1986 32.8374 23.4686C32.4369 23.9791 32.2132 24.2841 31.8402 24.6523C31.4828 25.005 31.1302 25.1777 30.8799 25.2783C30.4928 25.4337 30.0788 25.5121 29.663 25.546C29.3087 25.5749 29.0702 25.5934 28.7179 25.546C28.2786 25.4868 28.2472 25.4717 27.7214 25.2783C27.5003 25.197 27.131 24.9776 26.783 24.7266C26.3964 24.4478 26.1328 24.1992 25.6981 23.7395C25.3155 23.3347 25.1467 23.0947 24.8373 22.6316C24.5125 22.1455 24.2598 21.8766 23.953 21.379C19.7839 14.6168 13.8639 3.71523 13.8639 3.71523H5.99112L23.3635 33.9098L26.6885 28.1777C26.6885 28.1777 27.1444 28.7994 28.7438 28.7475C30.2137 28.6998 30.5672 28.1777 30.5672 28.1777L23.3635 40.437L3.53103 6.05657L2.19899 3.71523L0.345215 0.450195Z"
fill="white" />
fill="white"
/>
<circle cx="29.3282" cy="10.6089" r="3.34281" fill="white" />
</svg>
</template>
@@ -30,5 +47,5 @@
<script setup lang="ts">
defineOptions({
name: 'VueBitsLogo'
})
});
</script>

View File

@@ -1,25 +1,26 @@
<template>
<div class="preview-color">
<span class="color-label">{{ title }}</span>
<input :value="modelValue" @input="handleColorChange" type="color" :disabled="disabled" class="color-input" />
</div>
</template>
<script setup lang="ts">
defineProps<{
title: string
modelValue: string
disabled?: boolean
}>()
title: string;
modelValue: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
'update:modelValue': [value: string];
}>();
const handleColorChange = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
};
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="preview-select">
<span class="select-label">{{ title }}</span>
<Select
:model-value="modelValue"
@update:model-value="handleSelectChange"
@@ -14,45 +15,45 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import Select from 'primevue/select'
import { computed } from 'vue';
import Select from 'primevue/select';
interface Option {
label: string
value: string | number
label: string;
value: string | number;
}
const props = defineProps<{
title: string
modelValue: string | number
options: Option[] | string[] | number[]
optionLabel?: string
optionValue?: string
placeholder?: string
disabled?: boolean
}>()
title: string;
modelValue: string | number;
options: Option[] | string[] | number[];
optionLabel?: string;
optionValue?: string;
placeholder?: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string | number]
}>()
'update:modelValue': [value: string | number];
}>();
const handleSelectChange = (value: string | number) => {
emit('update:modelValue', value)
}
emit('update:modelValue', value);
};
const isObjectArray = computed(() => {
return props.options.length > 0 && typeof props.options[0] === 'object'
})
return props.options.length > 0 && typeof props.options[0] === 'object';
});
const selectAttributes = computed(() => {
if (isObjectArray.value) {
return {
optionLabel: props.optionLabel || 'label',
optionValue: props.optionValue || 'value'
};
}
}
return {}
})
return {};
});
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="preview-slider">
<span class="slider-label">{{ title }}</span>
<Slider
:model-value="modelValue"
@update:model-value="handleSliderChange"
@@ -10,31 +11,32 @@
:disabled="disabled"
class="custom-slider"
/>
<span class="slider-value">{{ modelValue }}{{ valueUnit }}</span>
</div>
</template>
<script setup lang="ts">
import Slider from 'primevue/slider'
import Slider from 'primevue/slider';
defineProps<{
title: string
modelValue: number
min?: number
max?: number
step?: number
valueUnit?: string
disabled?: boolean
}>()
title: string;
modelValue: number;
min?: number;
max?: number;
step?: number;
valueUnit?: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
'update:modelValue': [value: number];
}>();
const handleSliderChange = (value: number | number[]) => {
const numValue = Array.isArray(value) ? value[0] : value
emit('update:modelValue', numValue)
}
const numValue = Array.isArray(value) ? value[0] : value;
emit('update:modelValue', numValue);
};
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="preview-switch">
<span class="switch-label">{{ title }}</span>
<ToggleSwitch
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
@@ -10,17 +11,17 @@
</template>
<script setup lang="ts">
import ToggleSwitch from 'primevue/toggleswitch'
import ToggleSwitch from 'primevue/toggleswitch';
defineProps<{
title: string
modelValue: boolean
disabled?: boolean
}>()
title: string;
modelValue: boolean;
disabled?: boolean;
}>();
defineEmits<{
'update:modelValue': [value: boolean]
}>()
'update:modelValue': [value: boolean];
}>();
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="prop-table-container">
<h2 class="demo-title">Props</h2>
<div class="table-wrapper">
<DataTable :value="data" class="props-table">
<Column field="name" header="Property">
@@ -8,16 +9,19 @@
<div class="code-cell">{{ data.name }}</div>
</template>
</Column>
<Column field="type" header="Type">
<template #body="{ data }">
<span class="type-text">{{ data.type }}</span>
</template>
</Column>
<Column field="default" header="Default">
<template #body="{ data }">
<div class="code-cell">{{ data.default || '—' }}</div>
</template>
</Column>
<Column field="description" header="Description">
<template #body="{ data }">
<div class="description-text">{{ data.description }}</div>
@@ -29,19 +33,19 @@
</template>
<script setup lang="ts">
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
interface PropData {
name: string
type: string
default: string
description: string
name: string;
type: string;
default: string;
description: string;
}
defineProps<{
data: PropData[]
}>()
data: PropData[];
}>();
</script>
<style scoped>

View File

@@ -5,11 +5,11 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Button from 'primevue/button';
defineEmits<{
refresh: []
}>()
refresh: [];
}>();
</script>
<style scoped>

View File

@@ -5,24 +5,31 @@
<Tab value="0">
<div class="tab-header">
<i class="pi pi-eye"></i>
<span>Preview</span>
</div>
</Tab>
<Tab value="1">
<div class="tab-header">
<i class="pi pi-code"></i>
<span>Code</span>
</div>
</Tab>
<Tab value="2">
<div class="tab-header">
<i class="pi pi-box"></i>
<span>CLI</span>
</div>
</Tab>
<Tab value="3">
<div class="tab-header">
<i class="pi pi-heart"></i>
<span>Contribute</span>
</div>
</Tab>
@@ -32,12 +39,15 @@
<TabPanel value="0">
<slot name="preview" />
</TabPanel>
<TabPanel value="1">
<slot name="code" />
</TabPanel>
<TabPanel value="2">
<slot name="cli" />
</TabPanel>
<TabPanel value="3">
<ContributionSection />
</TabPanel>
@@ -47,12 +57,12 @@
</template>
<script setup lang="ts">
import Tabs from 'primevue/tabs'
import TabList from 'primevue/tablist'
import Tab from 'primevue/tab'
import TabPanels from 'primevue/tabpanels'
import TabPanel from 'primevue/tabpanel'
import ContributionSection from './ContributionSection.vue'
import Tabs from 'primevue/tabs';
import TabList from 'primevue/tablist';
import Tab from 'primevue/tab';
import TabPanels from 'primevue/tabpanels';
import TabPanel from 'primevue/tabpanel';
import ContributionSection from './ContributionSection.vue';
</script>
<style scoped>
@@ -121,8 +131,8 @@ import ContributionSection from './ContributionSection.vue'
:deep(.p-tab-indicator),
:deep(.p-tab::before),
:deep(.p-tab::after),
:deep(.p-tab[aria-selected="true"]::before),
:deep(.p-tab[aria-selected="true"]::after),
:deep(.p-tab[aria-selected='true']::before),
:deep(.p-tab[aria-selected='true']::after),
:deep(.p-tablist::after),
:deep(.p-tablist-tab-list::before),
:deep(.p-tablist-tab-list::after),
@@ -131,14 +141,14 @@ import ContributionSection from './ContributionSection.vue'
display: none !important;
}
:deep(.p-tab[aria-selected="true"]) {
:deep(.p-tab[aria-selected='true']) {
background: transparent !important;
border-bottom: none !important;
}
:deep(.p-tab[aria-selected="true"] .tab-header) {
:deep(.p-tab[aria-selected='true'] .tab-header) {
background: #333333;
color: #A7EF9E;
color: #a7ef9e;
}
:deep(.p-tabpanels) {

View File

@@ -77,7 +77,9 @@
color: inherit;
font-weight: 400;
opacity: 0.6;
transition: opacity 0.3s ease, transform 0.2s ease;
transition:
opacity 0.3s ease,
transform 0.2s ease;
}
.nav-link:hover {
@@ -106,9 +108,7 @@
font-weight: 500;
padding: 0 0 0 1.4rem;
height: calc(60px - 2px);
background: linear-gradient(135deg,
rgb(30, 160, 63),
rgba(24, 47, 255, 0.6));
background: linear-gradient(135deg, rgb(30, 160, 63), rgba(24, 47, 255, 0.6));
background-size: 200% 200%;
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
@@ -121,14 +121,14 @@
align-items: center;
white-space: nowrap;
justify-content: space-between;
transition: .3s ease;
transition: 0.3s ease;
}
.cta-button span {
background-color: #0b0b0b;
margin-left: 1em;
margin-right: calc(1em - 8px);
padding-top: .1em;
padding-top: 0.1em;
height: 45px;
border-radius: 50px;
width: 100px;
@@ -143,16 +143,16 @@
margin-right: 6px;
width: 16px;
height: 16px;
transition: .3s ease;
transition: 0.3s ease;
}
.cta-button:hover {
transition: .3s ease;
transition: 0.3s ease;
}
.cta-button:hover span img {
transform: scale(1.2);
transition: .3s ease;
transition: 0.3s ease;
}
@media (max-width: 900px) {

View File

@@ -7,22 +7,12 @@
<div class="nav-cta-group">
<nav class="landing-nav-items" ref="navRef">
<router-link
class="nav-link"
:class="{ 'active-link': activeItem === 'home' }"
to="/"
>
Home
</router-link>
<router-link class="nav-link" to="/text-animations/split-text">
Docs
</router-link>
<router-link class="nav-link" :class="{ 'active-link': activeItem === 'home' }" to="/">Home</router-link>
<router-link class="nav-link" to="/text-animations/split-text">Docs</router-link>
</nav>
<button
class="cta-button"
@click="openGitHub"
>
<button class="cta-button" @click="openGitHub">
Star On GitHub
<span ref="starCountRef" :style="{ opacity: 0 }">
<img :src="starIcon" alt="Star Icon" />
@@ -35,30 +25,33 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { gsap } from 'gsap'
import VueBitsLogo from '@/components/common/Logo.vue'
import { useStars } from '@/composables/useStars'
import starIcon from '@/assets/common/star.svg'
import './DisplayHeader.css'
import { ref, watch } from 'vue';
import { gsap } from 'gsap';
import VueBitsLogo from '@/components/common/Logo.vue';
import { useStars } from '@/composables/useStars';
import starIcon from '@/assets/common/star.svg';
import './DisplayHeader.css';
interface Props {
activeItem?: string | null;
}
defineProps<Props>()
defineProps<Props>();
const navRef = ref<HTMLElement | null>(null)
const starCountRef = ref<HTMLElement | null>(null)
const stars = useStars()
const navRef = ref<HTMLElement | null>(null);
const starCountRef = ref<HTMLElement | null>(null);
const stars = useStars();
const openGitHub = () => {
window.open('https://github.com/DavidHDev/vue-bits', '_blank')
}
window.open('https://github.com/DavidHDev/vue-bits', '_blank');
};
watch(stars, (newStars) => {
watch(
stars,
newStars => {
if (newStars && starCountRef.value) {
gsap.fromTo(starCountRef.value,
gsap.fromTo(
starCountRef.value,
{
scale: 0,
width: 0,
@@ -66,12 +59,14 @@ watch(stars, (newStars) => {
},
{
scale: 1,
width: "100px",
width: '100px',
opacity: 1,
duration: 0.8,
ease: "back.out(1)"
ease: 'back.out(1)'
}
)
);
}
}, { immediate: true })
},
{ immediate: true }
);
</script>

View File

@@ -22,7 +22,7 @@
font-weight: 600;
letter-spacing: -2px;
color: #fff;
margin-bottom: .2rem;
margin-bottom: 0.2rem;
background: linear-gradient(135deg, #fff 0%, #60fa89 20%, #55f788 40%, #00ff62 60%, #55f799 80%, #fff 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
@@ -44,7 +44,6 @@
}
@keyframes gradientShift {
0%,
100% {
background-position: 0% 50%;
@@ -120,8 +119,6 @@
grid-column: 2 / 3;
grid-row: 2 / 3;
}
}
@media (min-width: 50rem) {
@@ -145,8 +142,6 @@
grid-column: 1 / 3;
grid-row: 2 / 3;
}
}
@media (min-width: 768px) and (max-width: 49.99rem) {
@@ -169,8 +164,6 @@
grid-column: 1 / 2;
grid-row: 2 / 3;
}
}
.feature-card {
@@ -212,14 +205,20 @@
position: absolute;
inset: 0;
padding: 1px;
background: radial-gradient(200px circle at var(--glow-x) var(--glow-y),
background: radial-gradient(
200px circle at var(--glow-x) var(--glow-y),
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60%);
transparent 60%
);
border-radius: inherit;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: subtract;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
transition: opacity 0.3s ease;
@@ -230,8 +229,6 @@
background: #07160a;
}
.feature-card:hover::before {
opacity: 1;
}
@@ -314,7 +311,9 @@
}
.feature-card.particle-container:hover {
box-shadow: 0 4px 20px rgba(24, 78, 42, 0.4), 0 0 30px rgba(0, 255, 76, 0.2);
box-shadow:
0 4px 20px rgba(24, 78, 42, 0.4),
0 0 30px rgba(0, 255, 76, 0.2);
background: #07160b;
}
@@ -481,8 +480,6 @@
z-index: 2;
}
@media (max-width: 479px) {
.features-section {
padding: 4rem 1rem 2rem;
@@ -510,8 +507,6 @@
font-size: 4rem;
}
.feature-card h3 {
font-size: 1rem;
margin-bottom: 0.5rem;
@@ -553,8 +548,6 @@
font-size: 4rem;
}
.feature-card h3 {
font-size: 0.95rem;
margin-bottom: 0.4rem;

View File

@@ -3,6 +3,7 @@
<div class="features-container">
<div class="features-header">
<h3 class="features-title">Zero cost, all the cool.</h3>
<p class="features-subtitle">Everything you need to add flair to your websites</p>
</div>
@@ -13,11 +14,16 @@
<div className="messages-gif-wrapper">
<img src="/assets/messages.gif" alt="Messages animation" className="messages-gif" />
</div>
<h2>
<template v-if="isMobile">100</template>
<CountUp v-else :to="100" />%
<CountUp v-else :to="100" />
%
</h2>
<h3>Free &amp; Open Source</h3>
<p>Loved by developers around the world</p>
</ParticleCard>
@@ -25,19 +31,24 @@
<div className="components-gif-wrapper">
<img src="/assets/components.gif" alt="Components animation" className="components-gif" />
</div>
<h2>
<template v-if="isMobile">40</template>
<CountUp v-else :to="40" />+
<CountUp v-else :to="40" />
+
</h2>
<h3>Curated Components</h3>
<p>Growing weekly &amp; only getting better</p>
</ParticleCard>
<ParticleCard class="feature-card card4" :disable-animations="isMobile">
<h2>
Modern
</h2>
<h2>Modern</h2>
<h3>Technologies</h3>
<p>TypeScript + Tailwind, ready to ship</p>
</ParticleCard>
</div>
@@ -46,26 +57,26 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, defineComponent, h } from 'vue'
import { gsap } from 'gsap'
import CountUp from '../../../content/Animations/CountUp/CountUp.vue'
import './FeatureCards.css'
import { ref, onMounted, onUnmounted, defineComponent, h } from 'vue';
import { gsap } from 'gsap';
import CountUp from '../../../content/Animations/CountUp/CountUp.vue';
import './FeatureCards.css';
const isMobile = ref(false)
const gridRef = ref<HTMLDivElement | null>(null)
const isMobile = ref(false);
const gridRef = ref<HTMLDivElement | null>(null);
const checkIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
isMobile.value = window.innerWidth <= 768;
};
onMounted(() => {
checkIsMobile()
window.addEventListener('resize', checkIsMobile)
})
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
});
onUnmounted(() => {
window.removeEventListener('resize', checkIsMobile)
})
window.removeEventListener('resize', checkIsMobile);
});
const ParticleCard = defineComponent({
name: 'ParticleCard',
@@ -76,114 +87,119 @@ const ParticleCard = defineComponent({
}
},
setup(props, { slots }) {
const cardRef = ref<HTMLDivElement | null>(null)
const particlesRef = ref<HTMLDivElement[]>([])
const timeoutsRef = ref<number[]>([])
const isHoveredRef = ref(false)
const memoizedParticles = ref<HTMLDivElement[]>([])
const particlesInit = ref(false)
const cardRef = ref<HTMLDivElement | null>(null);
const particlesRef = ref<HTMLDivElement[]>([]);
const timeoutsRef = ref<number[]>([]);
const isHoveredRef = ref(false);
const memoizedParticles = ref<HTMLDivElement[]>([]);
const particlesInit = ref(false);
const createParticle = (x: number, y: number): HTMLDivElement => {
const el = document.createElement('div')
el.className = 'particle'
const el = document.createElement('div');
el.className = 'particle';
el.style.cssText = `
position:absolute;width:4px;height:4px;border-radius:50%;
background:rgba(132,0,255,1);box-shadow:0 0 6px rgba(132,0,255,.6);
pointer-events:none;z-index:100;left:${x}px;top:${y}px;
`
return el
}
`;
return el;
};
const memoizeParticles = () => {
if (particlesInit.value || !cardRef.value) return
const { width, height } = cardRef.value.getBoundingClientRect()
if (particlesInit.value || !cardRef.value) return;
const { width, height } = cardRef.value.getBoundingClientRect();
Array.from({ length: 12 }).forEach(() => {
memoizedParticles.value.push(createParticle(Math.random() * width, Math.random() * height))
})
particlesInit.value = true
}
memoizedParticles.value.push(createParticle(Math.random() * width, Math.random() * height));
});
particlesInit.value = true;
};
const clearParticles = () => {
timeoutsRef.value.forEach(clearTimeout)
timeoutsRef.value = []
timeoutsRef.value.forEach(clearTimeout);
timeoutsRef.value = [];
particlesRef.value.forEach(p =>
gsap.to(p, {
scale: 0,
opacity: 0,
duration: 0.3,
ease: "back.in(1.7)",
ease: 'back.in(1.7)',
onComplete: () => {
if (p.parentNode) {
p.parentNode.removeChild(p)
p.parentNode.removeChild(p);
}
}
},
})
)
particlesRef.value = []
}
);
particlesRef.value = [];
};
const animateParticles = () => {
if (!cardRef.value || !isHoveredRef.value) return
if (!particlesInit.value) memoizeParticles()
if (!cardRef.value || !isHoveredRef.value) return;
if (!particlesInit.value) memoizeParticles();
memoizedParticles.value.forEach((particle, i) => {
const id = setTimeout(() => {
if (!isHoveredRef.value || !cardRef.value) return
const clone = particle.cloneNode(true) as HTMLDivElement
cardRef.value.appendChild(clone)
particlesRef.value.push(clone)
if (!isHoveredRef.value || !cardRef.value) return;
const clone = particle.cloneNode(true) as HTMLDivElement;
cardRef.value.appendChild(clone);
particlesRef.value.push(clone);
gsap.set(clone, { scale: 0, opacity: 0 })
gsap.to(clone, { scale: 1, opacity: 1, duration: 0.3, ease: "back.out(1.7)" })
gsap.set(clone, { scale: 0, opacity: 0 });
gsap.to(clone, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' });
gsap.to(clone, {
x: (Math.random() - 0.5) * 100,
y: (Math.random() - 0.5) * 100,
rotation: Math.random() * 360,
duration: 2 + Math.random() * 2,
ease: "none",
ease: 'none',
repeat: -1,
yoyo: true,
})
gsap.to(clone, { opacity: 0.3, duration: 1.5, ease: "power2.inOut", repeat: -1, yoyo: true })
}, i * 100)
timeoutsRef.value.push(id)
})
}
yoyo: true
});
gsap.to(clone, { opacity: 0.3, duration: 1.5, ease: 'power2.inOut', repeat: -1, yoyo: true });
}, i * 100);
timeoutsRef.value.push(id);
});
};
const handleMouseEnter = () => {
isHoveredRef.value = true
animateParticles()
}
isHoveredRef.value = true;
animateParticles();
};
const handleMouseLeave = () => {
isHoveredRef.value = false
clearParticles()
}
isHoveredRef.value = false;
clearParticles();
};
onMounted(() => {
if (props.disableAnimations || !cardRef.value) return
if (props.disableAnimations || !cardRef.value) return;
const node = cardRef.value
node.addEventListener('mouseenter', handleMouseEnter)
node.addEventListener('mouseleave', handleMouseLeave)
})
const node = cardRef.value;
node.addEventListener('mouseenter', handleMouseEnter);
node.addEventListener('mouseleave', handleMouseLeave);
});
onUnmounted(() => {
if (cardRef.value) {
cardRef.value.removeEventListener('mouseenter', handleMouseEnter)
cardRef.value.removeEventListener('mouseleave', handleMouseLeave)
cardRef.value.removeEventListener('mouseenter', handleMouseEnter);
cardRef.value.removeEventListener('mouseleave', handleMouseLeave);
}
isHoveredRef.value = false
clearParticles()
})
isHoveredRef.value = false;
clearParticles();
});
return () => h('div', {
return () =>
h(
'div',
{
ref: cardRef,
class: 'particle-container',
style: { position: 'relative', overflow: 'hidden' }
}, slots.default?.())
},
slots.default?.()
);
}
})
});
const GlobalSpotlight = defineComponent({
name: 'GlobalSpotlight',
@@ -198,89 +214,88 @@ const GlobalSpotlight = defineComponent({
}
},
setup(props) {
const spotlightRef = ref<HTMLDivElement | null>(null)
const isInsideSectionRef = ref(false)
const spotlightRef = ref<HTMLDivElement | null>(null);
const isInsideSectionRef = ref(false);
const handleMouseMove = (e: MouseEvent) => {
if (!spotlightRef.value || !props.gridRef.value) return
const section = props.gridRef.value.closest('.features-section')
const rect = section?.getBoundingClientRect()
if (!spotlightRef.value || !props.gridRef.value) return;
const section = props.gridRef.value.closest('.features-section');
const rect = section?.getBoundingClientRect();
const inside =
rect &&
e.clientX >= rect.left && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY <= rect.bottom
rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
isInsideSectionRef.value = inside
const cards = props.gridRef.value.querySelectorAll('.feature-card')
isInsideSectionRef.value = inside;
const cards = props.gridRef.value.querySelectorAll('.feature-card');
if (!inside) {
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
cards.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
return
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: 'power2.out' });
cards.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'));
return;
}
let minDist = Infinity
const prox = 100, fade = 150
let minDist = Infinity;
const prox = 100,
fade = 150;
cards.forEach((card: HTMLElement) => {
const r = card.getBoundingClientRect()
const cx = r.left + r.width / 2
const cy = r.top + r.height / 2
const d = Math.hypot(e.clientX - cx, e.clientY - cy) - Math.max(r.width, r.height) / 2
const ed = Math.max(0, d)
minDist = Math.min(minDist, ed)
const r = card.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const d = Math.hypot(e.clientX - cx, e.clientY - cy) - Math.max(r.width, r.height) / 2;
const ed = Math.max(0, d);
minDist = Math.min(minDist, ed);
const rx = ((e.clientX - r.left) / r.width) * 100
const ry = ((e.clientY - r.top) / r.height) * 100
let glow = 0
if (ed <= prox) glow = 1
else if (ed <= fade) glow = (fade - ed) / (fade - prox)
card.style.setProperty('--glow-x', `${rx}%`)
card.style.setProperty('--glow-y', `${ry}%`)
card.style.setProperty('--glow-intensity', String(glow))
})
const rx = ((e.clientX - r.left) / r.width) * 100;
const ry = ((e.clientY - r.top) / r.height) * 100;
let glow = 0;
if (ed <= prox) glow = 1;
else if (ed <= fade) glow = (fade - ed) / (fade - prox);
card.style.setProperty('--glow-x', `${rx}%`);
card.style.setProperty('--glow-y', `${ry}%`);
card.style.setProperty('--glow-intensity', String(glow));
});
gsap.to(spotlightRef.value, { left: e.clientX, top: e.clientY, duration: 0.1, ease: "power2.out" })
const target = minDist <= prox ? 0.8 : minDist <= fade ? ((fade - minDist) / (fade - prox)) * 0.8 : 0
gsap.to(spotlightRef.value, { opacity: target, duration: target > 0 ? 0.2 : 0.5, ease: "power2.out" })
}
gsap.to(spotlightRef.value, { left: e.clientX, top: e.clientY, duration: 0.1, ease: 'power2.out' });
const target = minDist <= prox ? 0.8 : minDist <= fade ? ((fade - minDist) / (fade - prox)) * 0.8 : 0;
gsap.to(spotlightRef.value, { opacity: target, duration: target > 0 ? 0.2 : 0.5, ease: 'power2.out' });
};
const handleMouseLeave = () => {
isInsideSectionRef.value = false
isInsideSectionRef.value = false;
props.gridRef.value
?.querySelectorAll('.feature-card')
.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'));
if (spotlightRef.value) {
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
}
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: 'power2.out' });
}
};
onMounted(() => {
if (props.disableAnimations || !props.gridRef?.value) return
if (props.disableAnimations || !props.gridRef?.value) return;
const spotlight = document.createElement('div')
spotlight.className = 'global-spotlight'
const spotlight = document.createElement('div');
spotlight.className = 'global-spotlight';
spotlight.style.cssText = `
position:fixed;width:800px;height:800px;border-radius:50%;pointer-events:none;
background:radial-gradient(circle,rgba(132,0,255,.15) 0%,rgba(132,0,255,.08) 15%,
rgba(132,0,255,.04) 25%,rgba(132,0,255,.02) 40%,rgba(132,0,255,.01) 65%,transparent 70%);
z-index:200;opacity:0;transform:translate(-50%,-50%);mix-blend-mode:screen;
`
document.body.appendChild(spotlight)
spotlightRef.value = spotlight
`;
document.body.appendChild(spotlight);
spotlightRef.value = spotlight;
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseleave', handleMouseLeave)
})
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseleave', handleMouseLeave);
});
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseleave', handleMouseLeave)
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseleave', handleMouseLeave);
if (spotlightRef.value?.parentNode) {
spotlightRef.value.parentNode.removeChild(spotlightRef.value)
spotlightRef.value.parentNode.removeChild(spotlightRef.value);
}
})
});
return () => null
return () => null;
}
})
});
</script>

View File

@@ -48,13 +48,13 @@
}
.footer-heart {
color: #27FF64;
color: #27ff64;
font-size: 1em;
display: inline-block;
}
.footer-creator-link {
color: #27FF64;
color: #27ff64;
text-decoration: none;
transition: color 0.2s ease;
}

View File

@@ -4,10 +4,14 @@
<div class="footer-content">
<div class="footer-left">
<img :src="vueBitsLogo" alt="Vue Bits" class="footer-logo" />
<p class="footer-description">
A library created with <i class="pi pi-heart-fill footer-heart"></i> by
A library created with
<i class="pi pi-heart-fill footer-heart"></i>
by
<a href="https://davidhaz.com/" target="_blank" class="footer-creator-link">this guy</a>
</p>
<p class="footer-copyright">© {{ currentYear }} Vue Bits</p>
</div>
@@ -15,15 +19,12 @@
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" rel="noopener noreferrer" class="footer-link">
GitHub
</a>
<router-link to="/text-animations/split-text" class="footer-link">
Docs
</router-link>
<a href="https://www.jsrepo.com/" target="_blank" class="footer-link">
CLI
</a>
<a href="https://reactbits.dev/" target="_blank" class="footer-link">
React Bits
</a>
<router-link to="/text-animations/split-text" class="footer-link">Docs</router-link>
<a href="https://www.jsrepo.com/" target="_blank" class="footer-link">CLI</a>
<a href="https://reactbits.dev/" target="_blank" class="footer-link">React Bits</a>
</div>
</div>
</footer>
@@ -31,10 +32,10 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import vueBitsLogo from '../../../assets/logos/vue-bits-logo.svg'
import FadeContent from '@/content/Animations/FadeContent/FadeContent.vue'
import './Footer.css'
import { computed } from 'vue';
import vueBitsLogo from '../../../assets/logos/vue-bits-logo.svg';
import FadeContent from '@/content/Animations/FadeContent/FadeContent.vue';
import './Footer.css';
const currentYear = computed(() => new Date().getFullYear())
const currentYear = computed(() => new Date().getFullYear());
</script>

View File

@@ -2,18 +2,41 @@
<div class="landing-content">
<div class="hero-main-content">
<h1 class="landing-title">
<ResponsiveSplitText :is-mobile="isMobile" text="Animated Vue components" class-name="hero-split"
split-type="chars" :delay="30" :duration="2" ease="elastic.out(0.5, 0.3)" />
<ResponsiveSplitText
:is-mobile="isMobile"
text="Animated Vue components"
class-name="hero-split"
split-type="chars"
:delay="30"
:duration="2"
ease="elastic.out(0.5, 0.3)"
/>
<br />
<ResponsiveSplitText :is-mobile="isMobile" text="for creative developers" class-name="hero-split"
split-type="chars" :delay="30" :duration="2" ease="elastic.out(0.5, 0.3)" />
<ResponsiveSplitText
:is-mobile="isMobile"
text="for creative developers"
class-name="hero-split"
split-type="chars"
:delay="30"
:duration="2"
ease="elastic.out(0.5, 0.3)"
/>
</h1>
<ResponsiveSplitText :is-mobile="isMobile" class-name="landing-subtitle" split-type="words" :delay="10"
:duration="1" text="Eighty-plus snippets, ready to be dropped into your Vue projects" />
<ResponsiveSplitText
:is-mobile="isMobile"
class-name="landing-subtitle"
split-type="words"
:delay="10"
:duration="1"
text="Eighty-plus snippets, ready to be dropped into your Vue projects"
/>
<router-link to="/text-animations/split-text" class="landing-button">
<span>Browse Components</span>
<div class="button-arrow-circle">
<svg width="16" height="16" viewBox="0 0 16 16" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
<path d="M6 12L10 8L6 4" stroke="#0b0b0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -25,8 +48,14 @@
<div v-if="!isMobile" class="hero-cards-container">
<div class="hero-card hero-card-1" @click="openUrl('https://vue-bits.dev/backgrounds/dot-grid')">
<div class="w-full h-full relative hero-dot-grid">
<DotGrid base-color="#ffffff" active-color="rgba(138, 43, 226, 0.9)" :dot-size="8" :gap="16"
:proximity="50" />
<DotGrid
base-color="#ffffff"
active-color="rgba(138, 43, 226, 0.9)"
:dot-size="8"
:gap="16"
:proximity="50"
/>
<div class="placeholder-card"></div>
</div>
</div>
@@ -34,11 +63,13 @@
<div class="hero-cards-row">
<div class="hero-card hero-card-2" @click="openUrl('https://vue-bits.dev/backgrounds/letter-glitch')">
<LetterGlitch class-name="hero-glitch" :glitch-colors="['#ffffff', '#999999', '#333333']" />
<div class="placeholder-card"></div>
</div>
<div class="hero-card hero-card-3" @click="openUrl('https://vue-bits.dev/backgrounds/squares')">
<Squares border-color="#fff" :speed="0.2" direction="diagonal" hover-fill-color="#fff" />
<div class="placeholder-card"></div>
</div>
</div>
@@ -47,11 +78,11 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, h, defineComponent } from 'vue'
import DotGrid from '@/content/Backgrounds/DotGrid/DotGrid.vue'
import SplitText from '@/content/TextAnimations/SplitText/SplitText.vue'
import LetterGlitch from '@/content/Backgrounds/LetterGlitch/LetterGlitch.vue'
import Squares from '@/content/Backgrounds/Squares/Squares.vue'
import { ref, onMounted, onUnmounted, h, defineComponent } from 'vue';
import DotGrid from '@/content/Backgrounds/DotGrid/DotGrid.vue';
import SplitText from '@/content/TextAnimations/SplitText/SplitText.vue';
import LetterGlitch from '@/content/Backgrounds/LetterGlitch/LetterGlitch.vue';
import Squares from '@/content/Backgrounds/Squares/Squares.vue';
const ResponsiveSplitText = defineComponent({
props: {
@@ -71,7 +102,7 @@ const ResponsiveSplitText = defineComponent({
},
render() {
if (this.isMobile) {
return h('span', { class: this.className }, this.text)
return h('span', { class: this.className }, this.text);
} else {
return h(SplitText, {
text: this.text,
@@ -86,29 +117,29 @@ const ResponsiveSplitText = defineComponent({
rootMargin: this.rootMargin,
textAlign: this.textAlign,
onLetterAnimationComplete: this.onLetterAnimationComplete as (() => void) | undefined
})
});
}
}
})
});
const openUrl = (url: string) => {
window.open(url)
}
window.open(url);
};
const isMobile = ref(false)
const isMobile = ref(false);
const checkIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
isMobile.value = window.innerWidth <= 768;
};
onMounted(() => {
checkIsMobile()
window.addEventListener('resize', checkIsMobile)
})
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
});
onUnmounted(() => {
window.removeEventListener('resize', checkIsMobile)
})
window.removeEventListener('resize', checkIsMobile);
});
</script>
<style scoped>

View File

@@ -1,29 +1,32 @@
<template>
<div v-if="!isMobile" ref="containerRef" :style="{
<div
v-if="!isMobile"
ref="containerRef"
:style="{
position: 'absolute',
inset: 0,
overflow: 'hidden',
width: '100vw',
height: '100vh'
}">
</div>
}"
></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { Renderer, Camera, Transform, Program, Mesh, Geometry } from 'ogl'
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Renderer, Camera, Transform, Program, Mesh, Geometry } from 'ogl';
interface Props {
xOffset?: number
yOffset?: number
rotationDeg?: number
focalLength?: number
speed1?: number
speed2?: number
dir2?: number
bend1?: number
bend2?: number
fadeInDuration?: number
xOffset?: number;
yOffset?: number;
rotationDeg?: number;
focalLength?: number;
speed1?: number;
speed2?: number;
dir2?: number;
bend1?: number;
bend2?: number;
fadeInDuration?: number;
}
const props = withDefaults(defineProps<Props>(), {
@@ -37,7 +40,7 @@ const props = withDefaults(defineProps<Props>(), {
bend1: 0.9,
bend2: 0.6,
fadeInDuration: 2000
})
});
const vertex = /* glsl */ `
attribute vec2 position;
@@ -46,7 +49,7 @@ void main() {
vUv = position * 0.5 + 0.5;
gl_Position = vec4(position, 0.0, 1.0);
}
`
`;
const fragment = /* glsl */ `
precision mediump float;
@@ -143,40 +146,40 @@ void main() {
mainImage(color, coord);
gl_FragColor = color;
}
`
`;
const isMobile = ref(false)
const isVisible = ref(true)
const containerRef = ref<HTMLDivElement | null>(null)
const uniformOffset = ref(new Float32Array([props.xOffset, props.yOffset]))
const uniformResolution = ref(new Float32Array([1, 1]))
const rendererRef = ref<Renderer | null>(null)
const fadeStartTime = ref<number | null>(null)
const lastTimeRef = ref(0)
const pausedTimeRef = ref(0)
const rafId = ref<number | null>(null)
const resizeObserver = ref<ResizeObserver | null>(null)
const intersectionObserver = ref<IntersectionObserver | null>(null)
const isMobile = ref(false);
const isVisible = ref(true);
const containerRef = ref<HTMLDivElement | null>(null);
const uniformOffset = ref(new Float32Array([props.xOffset, props.yOffset]));
const uniformResolution = ref(new Float32Array([1, 1]));
const rendererRef = ref<Renderer | null>(null);
const fadeStartTime = ref<number | null>(null);
const lastTimeRef = ref(0);
const pausedTimeRef = ref(0);
const rafId = ref<number | null>(null);
const resizeObserver = ref<ResizeObserver | null>(null);
const intersectionObserver = ref<IntersectionObserver | null>(null);
const checkIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
isMobile.value = window.innerWidth <= 768;
};
const resize = () => {
if (!containerRef.value || !rendererRef.value) return
if (!containerRef.value || !rendererRef.value) return;
const { width, height } = containerRef.value.getBoundingClientRect()
rendererRef.value.setSize(width, height)
uniformResolution.value[0] = width * rendererRef.value.dpr
uniformResolution.value[1] = height * rendererRef.value.dpr
const { width, height } = containerRef.value.getBoundingClientRect();
rendererRef.value.setSize(width, height);
uniformResolution.value[0] = width * rendererRef.value.dpr;
uniformResolution.value[1] = height * rendererRef.value.dpr;
const gl = rendererRef.value.gl
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
gl.clear(gl.COLOR_BUFFER_BIT)
}
const gl = rendererRef.value.gl;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clear(gl.COLOR_BUFFER_BIT);
};
const initWebGL = () => {
if (isMobile.value || !containerRef.value) return
if (isMobile.value || !containerRef.value) return;
const renderer = new Renderer({
alpha: true,
@@ -184,20 +187,20 @@ const initWebGL = () => {
antialias: false,
depth: false,
stencil: false,
powerPreference: 'high-performance',
})
rendererRef.value = renderer
powerPreference: 'high-performance'
});
rendererRef.value = renderer;
const gl = renderer.gl
gl.clearColor(0, 0, 0, 0)
containerRef.value.appendChild(gl.canvas)
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
containerRef.value.appendChild(gl.canvas);
const camera = new Camera(gl)
const scene = new Transform()
const camera = new Camera(gl);
const scene = new Transform();
const geometry = new Geometry(gl, {
position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) },
})
position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) }
});
const program = new Program(gl, {
vertex,
@@ -215,117 +218,117 @@ const initWebGL = () => {
bend2: { value: props.bend2 },
bendAdj1: { value: 0 },
bendAdj2: { value: 0 },
uOpacity: { value: 0 },
},
})
new Mesh(gl, { geometry, program }).setParent(scene)
uOpacity: { value: 0 }
}
});
new Mesh(gl, { geometry, program }).setParent(scene);
resize()
resize();
resizeObserver.value = new ResizeObserver(resize)
resizeObserver.value.observe(containerRef.value)
resizeObserver.value = new ResizeObserver(resize);
resizeObserver.value.observe(containerRef.value);
const loop = (now: number) => {
if (isVisible.value) {
if (lastTimeRef.value === 0) {
lastTimeRef.value = now - pausedTimeRef.value
lastTimeRef.value = now - pausedTimeRef.value;
}
const t = (now - lastTimeRef.value) * 0.001
const t = (now - lastTimeRef.value) * 0.001;
if (fadeStartTime.value === null && t > 0.1) {
fadeStartTime.value = now
fadeStartTime.value = now;
}
let opacity = 0
let opacity = 0;
if (fadeStartTime.value !== null) {
const fadeElapsed = now - fadeStartTime.value
opacity = Math.min(fadeElapsed / props.fadeInDuration, 1)
opacity = 1 - Math.pow(1 - opacity, 3)
const fadeElapsed = now - fadeStartTime.value;
opacity = Math.min(fadeElapsed / props.fadeInDuration, 1);
opacity = 1 - Math.pow(1 - opacity, 3);
}
uniformOffset.value[0] = props.xOffset
uniformOffset.value[1] = props.yOffset
uniformOffset.value[0] = props.xOffset;
uniformOffset.value[1] = props.yOffset;
program.uniforms.iTime.value = t
program.uniforms.uRotation.value = props.rotationDeg * Math.PI / 180
program.uniforms.focalLength.value = props.focalLength
program.uniforms.uOpacity.value = opacity
program.uniforms.iTime.value = t;
program.uniforms.uRotation.value = (props.rotationDeg * Math.PI) / 180;
program.uniforms.focalLength.value = props.focalLength;
program.uniforms.uOpacity.value = opacity;
renderer.render({ scene, camera })
renderer.render({ scene, camera });
} else {
if (lastTimeRef.value !== 0) {
pausedTimeRef.value = now - lastTimeRef.value
lastTimeRef.value = 0
pausedTimeRef.value = now - lastTimeRef.value;
lastTimeRef.value = 0;
}
}
rafId.value = requestAnimationFrame(loop)
}
rafId.value = requestAnimationFrame(loop);
};
rafId.value = requestAnimationFrame(loop)
}
rafId.value = requestAnimationFrame(loop);
};
const setupIntersectionObserver = () => {
if (!containerRef.value || isMobile.value) return
if (!containerRef.value || isMobile.value) return;
intersectionObserver.value = new IntersectionObserver(
([entry]) => {
isVisible.value = entry.isIntersecting
isVisible.value = entry.isIntersecting;
},
{
rootMargin: '50px',
threshold: 0.1,
threshold: 0.1
}
)
);
intersectionObserver.value.observe(containerRef.value)
}
intersectionObserver.value.observe(containerRef.value);
};
const cleanup = () => {
if (rafId.value) {
cancelAnimationFrame(rafId.value)
rafId.value = null
cancelAnimationFrame(rafId.value);
rafId.value = null;
}
if (resizeObserver.value) {
resizeObserver.value.disconnect()
resizeObserver.value = null
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
if (intersectionObserver.value) {
intersectionObserver.value.disconnect()
intersectionObserver.value = null
intersectionObserver.value.disconnect();
intersectionObserver.value = null;
}
if (rendererRef.value) {
rendererRef.value.gl.canvas.remove()
rendererRef.value = null
rendererRef.value.gl.canvas.remove();
rendererRef.value = null;
}
window.removeEventListener('resize', checkIsMobile)
}
window.removeEventListener('resize', checkIsMobile);
};
onMounted(() => {
checkIsMobile()
window.addEventListener('resize', checkIsMobile)
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
if (!isMobile.value) {
initWebGL()
setupIntersectionObserver()
initWebGL();
setupIntersectionObserver();
}
})
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
watch(isMobile, (newIsMobile) => {
watch(isMobile, newIsMobile => {
if (newIsMobile) {
cleanup()
cleanup();
} else {
initWebGL()
setupIntersectionObserver()
initWebGL();
setupIntersectionObserver();
}
})
});
</script>

View File

@@ -17,9 +17,7 @@
max-width: 1200px;
user-select: none;
margin: 0 auto;
background: linear-gradient(135deg,
#3aed6d,
rgba(24, 255, 93, 0.6));
background: linear-gradient(135deg, #3aed6d, rgba(24, 255, 93, 0.6));
background-size: 200% 200%;
border-radius: 16px;
padding: 4rem 3rem;
@@ -78,7 +76,7 @@
background: transparent;
color: #0b0b0b;
border: 2px solid #0b0b0b;
padding: .6rem 1.6rem;
padding: 0.6rem 1.6rem;
font-size: 1.1rem;
font-weight: 600;
border-radius: 50px;
@@ -88,7 +86,7 @@
.start-building-button:hover {
background: #0b0b0b;
color: #27FF64;
color: #27ff64;
}
@media (max-width: 1280px) {

View File

@@ -3,16 +3,15 @@
<div class="start-building-container">
<div class="start-building-card">
<h2 class="start-building-title">Start exploring Vue Bits</h2>
<p class="start-building-subtitle">Animations, components, backgrounds - it's all here</p>
<router-link to="/text-animations/split-text" class="start-building-button">
Browse Components
</router-link>
<router-link to="/text-animations/split-text" class="start-building-button">Browse Components</router-link>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import './StartBuilding.css'
import './StartBuilding.css';
</script>

View File

@@ -1,20 +1,24 @@
<template>
<main class="app-container">
<Header />
<section class="category-wrapper">
<Sidebar />
<div class="category-page">
<router-view />
</div>
</section>
<Toast position="bottom-right"
<Toast
position="bottom-right"
:closeButtonProps="{ style: { right: '0', margin: '0', outline: 'none', border: 'none' } }"
:pt="{
message: {
style: {
borderRadius: '10px',
border: '1px solid #142216',
backgroundColor: '#0b0b0b',
backgroundColor: '#0b0b0b'
}
},
messageContent: {
@@ -27,7 +31,8 @@
display: 'none'
}
}
}" />
}"
/>
</main>
</template>

View File

@@ -28,6 +28,7 @@
<div class="drawer-content" @click.stop>
<div class="drawer-header">
<img :src="Logo" alt="Logo" class="drawer-logo" />
<button class="close-button" aria-label="Close Menu" @click="closeDrawer">
<i class="pi pi-times"></i>
</button>
@@ -35,6 +36,7 @@
<div class="drawer-body">
<!-- Navigation Categories -->
<div class="drawer-navigation">
<div class="categories-container">
<Category
@@ -55,9 +57,9 @@
<div class="drawer-section">
<p class="section-title">Useful Links</p>
<router-link to="/text-animations/split-text" @click="closeDrawer" class="drawer-link">
Docs
</router-link>
<router-link to="/text-animations/split-text" @click="closeDrawer" class="drawer-link">Docs</router-link>
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" @click="closeDrawer" class="drawer-link">
GitHub
<i class="pi pi-arrow-up-right arrow-icon"></i>
@@ -68,6 +70,7 @@
<div class="drawer-section">
<p class="section-title">Other</p>
<a href="https://davidhaz.com/" target="_blank" @click="closeDrawer" class="drawer-link">
Who made this?
<i class="pi pi-arrow-up-right arrow-icon"></i>
@@ -80,58 +83,58 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, defineComponent, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStars } from '../../composables/useStars'
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories'
import FadeContent from '../../content/Animations/FadeContent/FadeContent.vue'
import Logo from '../../assets/logos/vue-bits-logo.svg'
import Star from '../../assets/common/star.svg'
import { ref, onMounted, onUnmounted, computed, defineComponent, h } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStars } from '../../composables/useStars';
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories';
import FadeContent from '../../content/Animations/FadeContent/FadeContent.vue';
import Logo from '../../assets/logos/vue-bits-logo.svg';
import Star from '../../assets/common/star.svg';
const isDrawerOpen = ref(false)
const isTransitioning = ref(false)
const stars = useStars()
const route = useRoute()
const router = useRouter()
const isDrawerOpen = ref(false);
const isTransitioning = ref(false);
const stars = useStars();
const route = useRoute();
const router = useRouter();
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
const slug = (str: string) => str.replace(/\s+/g, '-').toLowerCase();
const toggleDrawer = () => {
isDrawerOpen.value = !isDrawerOpen.value
}
isDrawerOpen.value = !isDrawerOpen.value;
};
const closeDrawer = () => {
isDrawerOpen.value = false
}
isDrawerOpen.value = false;
};
const openGitHub = () => {
window.open('https://github.com/DavidHDev/vue-bits', '_blank')
}
window.open('https://github.com/DavidHDev/vue-bits', '_blank');
};
const onNavClick = () => {
closeDrawer()
window.scrollTo(0, 0)
}
closeDrawer();
window.scrollTo(0, 0);
};
const handleMobileTransitionNavigation = async (path: string) => {
if (isTransitioning.value || route.path === path) return
if (isTransitioning.value || route.path === path) return;
closeDrawer()
isTransitioning.value = true
closeDrawer();
isTransitioning.value = true;
try {
await router.push(path)
window.scrollTo(0, 0)
await router.push(path);
window.scrollTo(0, 0);
} finally {
isTransitioning.value = false
}
isTransitioning.value = false;
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isDrawerOpen.value) {
closeDrawer()
}
closeDrawer();
}
};
const Category = defineComponent({
name: 'Category',
@@ -167,63 +170,67 @@ const Category = defineComponent({
},
setup(props) {
interface ItemType {
sub: string
path: string
isActive: boolean
isNew: boolean
isUpdated: boolean
sub: string;
path: string;
isActive: boolean;
isNew: boolean;
isUpdated: boolean;
}
const items = computed(() =>
props.category.subcategories.map((sub: string): ItemType => {
const path = `/${slug(props.category.name)}/${slug(sub)}`
const activePath = props.location.path
const path = `/${slug(props.category.name)}/${slug(sub)}`;
const activePath = props.location.path;
return {
sub,
path,
isActive: activePath === path,
isNew: (NEW as string[]).includes(sub),
isUpdated: (UPDATED as string[]).includes(sub),
}
isUpdated: (UPDATED as string[]).includes(sub)
};
})
)
);
return () => h('div', { class: 'category' }, [
return () =>
h('div', { class: 'category' }, [
h('p', { class: 'category-name' }, props.category.name),
h('div', { class: 'category-items' },
h(
'div',
{ class: 'category-items' },
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
return h('router-link', {
return h(
'router-link',
{
key: path,
to: path,
class: [
'sidebar-item',
{ 'active-sidebar-item': isActive },
{ 'transitioning': props.isTransitioning }
],
class: ['sidebar-item', { 'active-sidebar-item': isActive }, { transitioning: props.isTransitioning }],
onClick: (e: Event) => {
e.preventDefault()
props.handleTransitionNavigation(path)
e.preventDefault();
props.handleTransitionNavigation(path);
},
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
onMouseleave: props.onItemMouseLeave
}, {
default: () => [
},
{
default: () =>
[
sub,
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
].filter(Boolean)
})
}
);
})
)
])
]);
}
})
});
onMounted(() => {
document.addEventListener('keydown', handleKeyDown)
})
document.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown)
})
document.removeEventListener('keydown', handleKeyDown);
});
</script>

View File

@@ -1,5 +1,6 @@
<template>
<!-- Mobile Drawer -->
<div v-if="isDrawerOpen" class="drawer-overlay" @click="closeDrawer">
<div class="drawer-content" :class="{ 'drawer-open': isDrawerOpen }" @click.stop>
<div class="drawer-header sidebar-logo">
@@ -7,6 +8,7 @@
<router-link to="/" @click="closeDrawer">
<img :src="Logo" alt="Logo" class="drawer-logo" />
</router-link>
<button class="icon-button" aria-label="Close" @click="closeDrawer">
<i class="pi pi-times"></i>
</button>
@@ -15,26 +17,41 @@
<div class="drawer-body">
<div class="categories-container">
<Category v-for="cat in CATEGORIES" :key="cat.name" :category="cat" :location="route"
:pending-active-path="pendingActivePath ?? undefined" :handle-click="onNavClick"
:handle-transition-navigation="handleMobileTransitionNavigation" :on-item-mouse-enter="() => { }"
:on-item-mouse-leave="() => { }" :is-transitioning="isTransitioning" />
<Category
v-for="cat in CATEGORIES"
:key="cat.name"
:category="cat"
:location="route"
:pending-active-path="pendingActivePath ?? undefined"
:handle-click="onNavClick"
:handle-transition-navigation="handleMobileTransitionNavigation"
:on-item-mouse-enter="() => {}"
:on-item-mouse-leave="() => {}"
:is-transitioning="isTransitioning"
/>
</div>
<div class="separator"></div>
<div class="useful-links">
<p class="useful-links-title">Useful Links</p>
<div class="links-container">
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" @click="closeDrawer" class="useful-link">
<span>GitHub</span>
<i class="pi pi-arrow-up-right arrow-icon"></i>
</a>
<router-link to="/text-animations/split-text" @click="closeDrawer" class="useful-link">
<span>Docs</span>
<i class="pi pi-arrow-up-right arrow-icon"></i>
</router-link>
<a href="https://davidhaz.com/" target="_blank" @click="closeDrawer" class="useful-link">
<span>Who made this?</span>
<i class="pi pi-arrow-up-right arrow-icon"></i>
</a>
</div>
@@ -44,174 +61,192 @@
</div>
<!-- Desktop Sidebar -->
<nav ref="sidebarContainerRef" class="sidebar" :class="{ 'sidebar-no-fade': isScrolledToBottom }"
@scroll="handleScroll">
<nav
ref="sidebarContainerRef"
class="sidebar"
:class="{ 'sidebar-no-fade': isScrolledToBottom }"
@scroll="handleScroll"
>
<div ref="sidebarRef" class="sidebar-content">
<!-- Active line indicator -->
<div class="active-line" :style="{
transform: isLineVisible && linePosition !== null
? `translateY(${linePosition - 8}px)`
: 'translateY(-100px)',
<div
class="active-line"
:style="{
transform:
isLineVisible && linePosition !== null ? `translateY(${linePosition - 8}px)` : 'translateY(-100px)',
opacity: isLineVisible ? 1 : 0
}"></div>
}"
></div>
<!-- Hover line indicator -->
<div class="hover-line" :style="{
transform: hoverLinePosition !== null
? `translateY(${hoverLinePosition - 8}px)`
: 'translateY(-100px)',
<div
class="hover-line"
:style="{
transform: hoverLinePosition !== null ? `translateY(${hoverLinePosition - 8}px)` : 'translateY(-100px)',
opacity: isHoverLineVisible ? 1 : 0
}"></div>
}"
></div>
<div class="categories-list">
<Category v-for="cat in CATEGORIES" :key="cat.name" :category="cat" :location="route"
:pending-active-path="pendingActivePath ?? undefined" :handle-click="scrollToTop"
:handle-transition-navigation="handleTransitionNavigation" :on-item-mouse-enter="onItemEnter"
:on-item-mouse-leave="onItemLeave" :is-transitioning="isTransitioning" />
<Category
v-for="cat in CATEGORIES"
:key="cat.name"
:category="cat"
:location="route"
:pending-active-path="pendingActivePath ?? undefined"
:handle-click="scrollToTop"
:handle-transition-navigation="handleTransitionNavigation"
:on-item-mouse-enter="onItemEnter"
:on-item-mouse-leave="onItemLeave"
:is-transitioning="isTransitioning"
/>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, watch, defineComponent, h, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories'
import Logo from '../../assets/logos/vue-bits-logo.svg'
import '../../css/sidebar.css'
import { ref, onMounted, onUnmounted, nextTick, watch, defineComponent, h, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories';
import Logo from '../../assets/logos/vue-bits-logo.svg';
import '../../css/sidebar.css';
const HOVER_TIMEOUT_DELAY = 150
const HOVER_TIMEOUT_DELAY = 150;
const isDrawerOpen = ref(false)
const linePosition = ref<number | null>(null)
const isLineVisible = ref(false)
const hoverLinePosition = ref<number | null>(null)
const isHoverLineVisible = ref(false)
const pendingActivePath = ref<string | null>(null)
const isScrolledToBottom = ref(false)
const isTransitioning = ref(false)
const isDrawerOpen = ref(false);
const linePosition = ref<number | null>(null);
const isLineVisible = ref(false);
const hoverLinePosition = ref<number | null>(null);
const isHoverLineVisible = ref(false);
const pendingActivePath = ref<string | null>(null);
const isScrolledToBottom = ref(false);
const isTransitioning = ref(false);
const sidebarRef = ref<HTMLDivElement>()
const sidebarContainerRef = ref<HTMLDivElement>()
const sidebarRef = ref<HTMLDivElement>();
const sidebarContainerRef = ref<HTMLDivElement>();
let hoverTimeoutRef: number | null = null
let hoverDelayTimeoutRef: number | null = null
let hoverTimeoutRef: number | null = null;
let hoverDelayTimeoutRef: number | null = null;
const route = useRoute()
const router = useRouter()
const route = useRoute();
const router = useRouter();
const scrollToTop = () => window.scrollTo(0, 0)
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
const scrollToTop = () => window.scrollTo(0, 0);
const slug = (str: string) => str.replace(/\s+/g, '-').toLowerCase();
const findActiveElement = () => {
const activePath = pendingActivePath.value || route.path
const activePath = pendingActivePath.value || route.path;
for (const category of CATEGORIES) {
const activeItem = category.subcategories.find((sub: string) => {
const expectedPath = `/${slug(category.name)}/${slug(sub)}`
return activePath === expectedPath
})
const expectedPath = `/${slug(category.name)}/${slug(sub)}`;
return activePath === expectedPath;
});
if (activeItem) {
const selector = `.sidebar a[href="${activePath}"]`
const element = document.querySelector(selector) as HTMLElement
return element
const selector = `.sidebar a[href="${activePath}"]`;
const element = document.querySelector(selector) as HTMLElement;
return element;
}
}
return null
}
return null;
};
const updateLinePosition = (el: HTMLElement | null) => {
if (!el || !sidebarRef.value || !sidebarRef.value.offsetParent) return null
const sidebarRect = sidebarRef.value.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
return elRect.top - sidebarRect.top + elRect.height / 2
}
if (!el || !sidebarRef.value || !sidebarRef.value.offsetParent) return null;
const sidebarRect = sidebarRef.value.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
return elRect.top - sidebarRect.top + elRect.height / 2;
};
const closeDrawer = () => {
isDrawerOpen.value = false
}
isDrawerOpen.value = false;
};
const onNavClick = () => {
closeDrawer()
scrollToTop()
}
closeDrawer();
scrollToTop();
};
const handleTransitionNavigation = async (path: string) => {
if (isTransitioning.value || route.path === path) return
if (isTransitioning.value || route.path === path) return;
pendingActivePath.value = path
pendingActivePath.value = path;
// TODO: Implement transition when available
await router.push(path)
scrollToTop()
pendingActivePath.value = null
}
await router.push(path);
scrollToTop();
pendingActivePath.value = null;
};
const handleMobileTransitionNavigation = async (path: string) => {
if (isTransitioning.value || route.path === path) return
if (isTransitioning.value || route.path === path) return;
closeDrawer()
pendingActivePath.value = path
closeDrawer();
pendingActivePath.value = path;
// TODO: Implement transition when available
await router.push(path)
scrollToTop()
pendingActivePath.value = null
}
await router.push(path);
scrollToTop();
pendingActivePath.value = null;
};
const onItemEnter = (path: string, e: Event) => {
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef)
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef);
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef);
const targetElement = e.currentTarget as HTMLElement
const pos = updateLinePosition(targetElement)
const targetElement = e.currentTarget as HTMLElement;
const pos = updateLinePosition(targetElement);
if (pos !== null) {
hoverLinePosition.value = pos
hoverLinePosition.value = pos;
}
hoverDelayTimeoutRef = setTimeout(() => {
isHoverLineVisible.value = true
}, 200)
}
isHoverLineVisible.value = true;
}, 200);
};
const onItemLeave = () => {
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef);
hoverTimeoutRef = setTimeout(() => {
isHoverLineVisible.value = false
}, HOVER_TIMEOUT_DELAY)
}
isHoverLineVisible.value = false;
}, HOVER_TIMEOUT_DELAY);
};
const handleScroll = () => {
const sidebarElement = sidebarContainerRef.value
if (!sidebarElement) return
const sidebarElement = sidebarContainerRef.value;
if (!sidebarElement) return;
const { scrollTop, scrollHeight, clientHeight } = sidebarElement
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10
isScrolledToBottom.value = isAtBottom
}
const { scrollTop, scrollHeight, clientHeight } = sidebarElement;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10;
isScrolledToBottom.value = isAtBottom;
};
const updateActiveLine = async () => {
await nextTick()
await nextTick();
setTimeout(() => {
const activeEl = findActiveElement()
const activeEl = findActiveElement();
if (!activeEl) {
isLineVisible.value = false
return
isLineVisible.value = false;
return;
}
const pos = updateLinePosition(activeEl)
const pos = updateLinePosition(activeEl);
if (pos !== null) {
linePosition.value = pos
isLineVisible.value = true
linePosition.value = pos;
isLineVisible.value = true;
} else {
isLineVisible.value = false
}
}, 100)
isLineVisible.value = false;
}
}, 100);
};
const Category = defineComponent({
name: 'Category',
@@ -251,74 +286,78 @@ const Category = defineComponent({
},
setup(props) {
interface ItemType {
sub: string
path: string
isActive: boolean
isNew: boolean
isUpdated: boolean
sub: string;
path: string;
isActive: boolean;
isNew: boolean;
isUpdated: boolean;
}
const items = computed(() =>
props.category.subcategories.map((sub: string): ItemType => {
const path = `/${slug(props.category.name)}/${slug(sub)}`
const activePath = props.pendingActivePath || props.location.path
const path = `/${slug(props.category.name)}/${slug(sub)}`;
const activePath = props.pendingActivePath || props.location.path;
return {
sub,
path,
isActive: activePath === path,
isNew: (NEW as string[]).includes(sub),
isUpdated: (UPDATED as string[]).includes(sub),
}
isUpdated: (UPDATED as string[]).includes(sub)
};
})
)
);
return () => h('div', { class: 'category' }, [
return () =>
h('div', { class: 'category' }, [
h('p', { class: 'category-name' }, props.category.name),
h('div', { class: 'category-items' },
h(
'div',
{ class: 'category-items' },
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
return h('router-link', {
return h(
'router-link',
{
key: path,
to: path,
class: [
'sidebar-item',
{ 'active-sidebar-item': isActive },
{ 'transitioning': props.isTransitioning }
],
class: ['sidebar-item', { 'active-sidebar-item': isActive }, { transitioning: props.isTransitioning }],
onClick: (e: Event) => {
e.preventDefault()
props.handleTransitionNavigation(path)
e.preventDefault();
props.handleTransitionNavigation(path);
},
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
onMouseleave: props.onItemMouseLeave
}, {
default: () => [
},
{
default: () =>
[
sub,
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
].filter(Boolean)
})
}
);
})
)
])
]);
}
})
});
watch(() => route.path, updateActiveLine)
watch(pendingActivePath, updateActiveLine)
watch(() => route.path, updateActiveLine);
watch(pendingActivePath, updateActiveLine);
onMounted(() => {
updateActiveLine()
updateActiveLine();
if (sidebarContainerRef.value) {
sidebarContainerRef.value.addEventListener('scroll', handleScroll)
handleScroll()
sidebarContainerRef.value.addEventListener('scroll', handleScroll);
handleScroll();
}
})
});
onUnmounted(() => {
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef)
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef);
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef);
if (sidebarContainerRef.value) {
sidebarContainerRef.value.removeEventListener('scroll', handleScroll)
sidebarContainerRef.value.removeEventListener('scroll', handleScroll);
}
})
});
</script>

View File

@@ -1,18 +1,18 @@
import { ref } from 'vue'
import { ref } from 'vue';
/**
* Composable for force re-rendering components
* Useful for demo components that need to restart animations or reset state
*/
export function useForceRerender() {
const rerenderKey = ref(0)
const rerenderKey = ref(0);
const forceRerender = () => {
rerenderKey.value++
}
rerenderKey.value++;
};
return {
rerenderKey,
forceRerender
}
};
}

View File

@@ -1,48 +1,51 @@
import { ref, onMounted } from 'vue'
import { getStarsCount } from '@/utils/utils'
import { ref, onMounted } from 'vue';
import { getStarsCount } from '@/utils/utils';
const CACHE_KEY = 'github_stars_cache'
const CACHE_DURATION = 24 * 60 * 60 * 1000
const CACHE_KEY = 'github_stars_cache';
const CACHE_DURATION = 24 * 60 * 60 * 1000;
export function useStars() {
const stars = ref<number>(0)
const stars = ref<number>(0);
const fetchStars = async () => {
try {
const cachedData = localStorage.getItem(CACHE_KEY)
const cachedData = localStorage.getItem(CACHE_KEY);
if (cachedData) {
const { count, timestamp } = JSON.parse(cachedData)
const now = Date.now()
const { count, timestamp } = JSON.parse(cachedData);
const now = Date.now();
if (now - timestamp < CACHE_DURATION) {
stars.value = count
return
stars.value = count;
return;
}
}
const count = await getStarsCount()
const count = await getStarsCount();
localStorage.setItem(CACHE_KEY, JSON.stringify({
localStorage.setItem(
CACHE_KEY,
JSON.stringify({
count,
timestamp: Date.now()
}))
})
);
stars.value = count
stars.value = count;
} catch (error) {
console.error('Error fetching stars:', error)
console.error('Error fetching stars:', error);
const cachedData = localStorage.getItem(CACHE_KEY)
const cachedData = localStorage.getItem(CACHE_KEY);
if (cachedData) {
const { count } = JSON.parse(cachedData)
stars.value = count
}
const { count } = JSON.parse(cachedData);
stars.value = count;
}
}
};
onMounted(() => {
fetchStars()
})
fetchStars();
});
return stars
return stars;
}

View File

@@ -20,7 +20,7 @@ export const CATEGORIES = [
'Text Cursor',
'Decrypted Text',
'True Focus',
'Scroll Float',
'Scroll Float'
]
},
{
@@ -34,7 +34,7 @@ export const CATEGORIES = [
'Count Up',
'Click Spark',
'Magnet',
'Cubes',
'Cubes'
]
},
{
@@ -55,8 +55,8 @@ export const CATEGORIES = [
'Glass Icons',
'Decay Card',
'Flowing Menu',
'Elastic Slider',
],
'Elastic Slider'
]
},
{
name: 'Backgrounds',
@@ -73,6 +73,6 @@ export const CATEGORIES = [
'Iridescence',
'Threads',
'Grid Motion'
],
]
}
];

View File

@@ -1,69 +1,69 @@
const animations = {
'fade-content': () => import("../demo/Animations/FadeContentDemo.vue"),
'animated-content': () => import("../demo/Animations/AnimatedContentDemo.vue"),
'pixel-transition': () => import("../demo/Animations/PixelTransitionDemo.vue"),
'glare-hover': () => import("../demo/Animations/GlareHoverDemo.vue"),
'magnet-lines': () => import("../demo/Animations/MagnetLinesDemo.vue"),
'click-spark': () => import("../demo/Animations/ClickSparkDemo.vue"),
'magnet': () => import("../demo/Animations/MagnetDemo.vue"),
'cubes': () => import("../demo/Animations/CubesDemo.vue"),
'count-up': () => import("../demo/Animations/CountUpDemo.vue"),
'fade-content': () => import('../demo/Animations/FadeContentDemo.vue'),
'animated-content': () => import('../demo/Animations/AnimatedContentDemo.vue'),
'pixel-transition': () => import('../demo/Animations/PixelTransitionDemo.vue'),
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
magnet: () => import('../demo/Animations/MagnetDemo.vue'),
cubes: () => import('../demo/Animations/CubesDemo.vue'),
'count-up': () => import('../demo/Animations/CountUpDemo.vue')
};
const textAnimations = {
'split-text': () => import("../demo/TextAnimations/SplitTextDemo.vue"),
'blur-text': () => import("../demo/TextAnimations/BlurTextDemo.vue"),
'circular-text': () => import("../demo/TextAnimations/CircularTextDemo.vue"),
'shiny-text': () => import("../demo/TextAnimations/ShinyTextDemo.vue"),
'text-pressure': () => import("../demo/TextAnimations/TextPressureDemo.vue"),
'curved-loop': () => import("../demo/TextAnimations/CurvedLoopDemo.vue"),
'fuzzy-text': () => import("../demo/TextAnimations/FuzzyTextDemo.vue"),
'gradient-text': () => import("../demo/TextAnimations/GradientTextDemo.vue"),
'text-trail': () => import("../demo/TextAnimations/TextTrailDemo.vue"),
'falling-text': () => import("../demo/TextAnimations/FallingTextDemo.vue"),
'text-cursor': () => import("../demo/TextAnimations/TextCursorDemo.vue"),
'decrypted-text': () => import("../demo/TextAnimations/DecryptedTextDemo.vue"),
'true-focus': () => import("../demo/TextAnimations/TrueFocusDemo.vue"),
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo.vue"),
'split-text': () => import('../demo/TextAnimations/SplitTextDemo.vue'),
'blur-text': () => import('../demo/TextAnimations/BlurTextDemo.vue'),
'circular-text': () => import('../demo/TextAnimations/CircularTextDemo.vue'),
'shiny-text': () => import('../demo/TextAnimations/ShinyTextDemo.vue'),
'text-pressure': () => import('../demo/TextAnimations/TextPressureDemo.vue'),
'curved-loop': () => import('../demo/TextAnimations/CurvedLoopDemo.vue'),
'fuzzy-text': () => import('../demo/TextAnimations/FuzzyTextDemo.vue'),
'gradient-text': () => import('../demo/TextAnimations/GradientTextDemo.vue'),
'text-trail': () => import('../demo/TextAnimations/TextTrailDemo.vue'),
'falling-text': () => import('../demo/TextAnimations/FallingTextDemo.vue'),
'text-cursor': () => import('../demo/TextAnimations/TextCursorDemo.vue'),
'decrypted-text': () => import('../demo/TextAnimations/DecryptedTextDemo.vue'),
'true-focus': () => import('../demo/TextAnimations/TrueFocusDemo.vue'),
'scroll-float': () => import('../demo/TextAnimations/ScrollFloatDemo.vue')
};
const components = {
'masonry': () => import("../demo/Components/MasonryDemo.vue"),
'profile-card': () => import("../demo/Components/ProfileCardDemo.vue"),
'dock': () => import("../demo/Components/DockDemo.vue"),
'gooey-nav': () => import("../demo/Components/GooeyNavDemo.vue"),
'pixel-card': () => import("../demo/Components/PixelCardDemo.vue"),
'carousel': () => import("../demo/Components/CarouselDemo.vue"),
'spotlight-card': () => import("../demo/Components/SpotlightCardDemo.vue"),
'circular-gallery': () => import("../demo/Components/CircularGalleryDemo.vue"),
'flying-posters': () => import("../demo/Components/FlyingPostersDemo.vue"),
'card-swap': () => import("../demo/Components/CardSwapDemo.vue"),
'infinite-scroll': () => import("../demo/Components/InfiniteScrollDemo.vue"),
'glass-icons': () => import("../demo/Components/GlassIconsDemo.vue"),
'decay-card': () => import("../demo/Components/DecayCardDemo.vue"),
'flowing-menu': () => import("../demo/Components/FlowingMenuDemo.vue"),
'elastic-slider': () => import("../demo/Components/ElasticSliderDemo.vue"),
'tilted-card': () => import("../demo/Components/TiltedCardDemo.vue"),
masonry: () => import('../demo/Components/MasonryDemo.vue'),
'profile-card': () => import('../demo/Components/ProfileCardDemo.vue'),
dock: () => import('../demo/Components/DockDemo.vue'),
'gooey-nav': () => import('../demo/Components/GooeyNavDemo.vue'),
'pixel-card': () => import('../demo/Components/PixelCardDemo.vue'),
carousel: () => import('../demo/Components/CarouselDemo.vue'),
'spotlight-card': () => import('../demo/Components/SpotlightCardDemo.vue'),
'circular-gallery': () => import('../demo/Components/CircularGalleryDemo.vue'),
'flying-posters': () => import('../demo/Components/FlyingPostersDemo.vue'),
'card-swap': () => import('../demo/Components/CardSwapDemo.vue'),
'infinite-scroll': () => import('../demo/Components/InfiniteScrollDemo.vue'),
'glass-icons': () => import('../demo/Components/GlassIconsDemo.vue'),
'decay-card': () => import('../demo/Components/DecayCardDemo.vue'),
'flowing-menu': () => import('../demo/Components/FlowingMenuDemo.vue'),
'elastic-slider': () => import('../demo/Components/ElasticSliderDemo.vue'),
'tilted-card': () => import('../demo/Components/TiltedCardDemo.vue')
};
const backgrounds = {
'dot-grid': () => import("../demo/Backgrounds/DotGridDemo.vue"),
'silk': () => import("../demo/Backgrounds/SilkDemo.vue"),
'lightning': () => import("../demo/Backgrounds/LightningDemo.vue"),
'letter-glitch': () => import("../demo/Backgrounds/LetterGlitchDemo.vue"),
'particles': () => import("../demo/Backgrounds/ParticlesDemo.vue"),
'waves': () => import("../demo/Backgrounds/WavesDemo.vue"),
'squares': () => import("../demo/Backgrounds/SquaresDemo.vue"),
'iridescence': () => import("../demo/Backgrounds/IridescenceDemo.vue"),
'threads': () => import("../demo/Backgrounds/ThreadsDemo.vue"),
'aurora': () => import("../demo/Backgrounds/AuroraDemo.vue"),
'beams': () => import("../demo/Backgrounds/BeamsDemo.vue"),
'grid-motion': () => import("../demo/Backgrounds/GridMotionDemo.vue"),
'dot-grid': () => import('../demo/Backgrounds/DotGridDemo.vue'),
silk: () => import('../demo/Backgrounds/SilkDemo.vue'),
lightning: () => import('../demo/Backgrounds/LightningDemo.vue'),
'letter-glitch': () => import('../demo/Backgrounds/LetterGlitchDemo.vue'),
particles: () => import('../demo/Backgrounds/ParticlesDemo.vue'),
waves: () => import('../demo/Backgrounds/WavesDemo.vue'),
squares: () => import('../demo/Backgrounds/SquaresDemo.vue'),
iridescence: () => import('../demo/Backgrounds/IridescenceDemo.vue'),
threads: () => import('../demo/Backgrounds/ThreadsDemo.vue'),
aurora: () => import('../demo/Backgrounds/AuroraDemo.vue'),
beams: () => import('../demo/Backgrounds/BeamsDemo.vue'),
'grid-motion': () => import('../demo/Backgrounds/GridMotionDemo.vue')
};
export const componentMap = {
...animations,
...textAnimations,
...components,
...backgrounds,
...backgrounds
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/AnimatedContent/AnimatedContent.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/AnimatedContent/AnimatedContent.vue?raw';
import type { CodeObject } from '@/types/code';
export const animatedContent: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/AnimatedContent`,
@@ -32,4 +32,4 @@ export const animatedContent: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/ClickSpark/ClickSpark.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/ClickSpark/ClickSpark.vue?raw';
import type { CodeObject } from '../../../types/code';
export const clickSpark: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/ClickSpark`,
@@ -44,4 +44,4 @@ import ClickSpark from '@/content/Animations/ClickSpark/ClickSpark.vue'
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/CountUp/CountUp.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/CountUp/CountUp.vue?raw';
import type { CodeObject } from '@/types/code';
export const countup: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/CountUp`,
@@ -30,4 +30,4 @@ export const countup: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/Cubes/Cubes.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/Cubes/Cubes.vue?raw';
import type { CodeObject } from '../../../types/code';
export const cubes: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/Cubes`,
@@ -29,4 +29,4 @@ export const cubes: CodeObject = {
import Cubes from "./Cubes.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/FadeContent/FadeContent.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/FadeContent/FadeContent.vue?raw';
import type { CodeObject } from '../../../types/code';
export const fadeContent: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/FadeContent`,
@@ -24,4 +24,4 @@ export const fadeContent: CodeObject = {
import FadeContent from "./FadeContent.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/GlareHover/GlareHover.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/GlareHover/GlareHover.vue?raw';
import type { CodeObject } from '@/types/code';
export const glareHover: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/GlareHover`,
@@ -26,4 +26,4 @@ export const glareHover: CodeObject = {
import GlareHover from "./GlareHover.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/Magnet/Magnet.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/Magnet/Magnet.vue?raw';
import type { CodeObject } from '../../../types/code';
export const magnet: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/Magnet`,
@@ -45,4 +45,4 @@ import Magnet from '@/content/Animations/Magnet/Magnet.vue'
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/MagnetLines/MagnetLines.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/MagnetLines/MagnetLines.vue?raw';
import type { CodeObject } from '@/types/code';
export const magnetLines: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/MagnetLines`,
@@ -19,4 +19,4 @@ export const magnetLines: CodeObject = {
import MagnetLines from "./MagnetLines.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/PixelTransition/PixelTransition.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/PixelTransition/PixelTransition.vue?raw';
import type { CodeObject } from '@/types/code';
export const pixelTransition: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/PixelTransition`,
@@ -26,4 +26,4 @@ export const pixelTransition: CodeObject = {
import PixelTransition from './PixelTransition.vue';
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Aurora/Aurora.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Aurora/Aurora.vue?raw';
import type { CodeObject } from '../../../types/code';
export const aurora: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Aurora`,
@@ -30,4 +30,4 @@ export const aurora: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Beams/Beams.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Beams/Beams.vue?raw';
import type { CodeObject } from '../../../types/code';
export const beams: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Beams`,
@@ -33,4 +33,4 @@ export const beams: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/DotGrid/DotGrid.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/DotGrid/DotGrid.vue?raw';
import type { CodeObject } from '../../../types/code';
export const dotGrid: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/DotGrid`,
@@ -36,4 +36,4 @@ export const dotGrid: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Iridescence/Iridescence.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Iridescence/Iridescence.vue?raw';
import type { CodeObject } from '../../../types/code';
export const iridescence: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Iridescence`,
@@ -19,4 +19,4 @@ export const iridescence: CodeObject = {
import Iridescence from "./Iridescence.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/LetterGlitch/LetterGlitch.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/LetterGlitch/LetterGlitch.vue?raw';
import type { CodeObject } from '../../../types/code';
export const letterGlitch: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/LetterGlitch`,
@@ -29,4 +29,4 @@ export const letterGlitch: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Lightning/Lightning.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Lightning/Lightning.vue?raw';
import type { CodeObject } from '../../../types/code';
export const lightning: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Lightning`,
@@ -30,4 +30,4 @@ export const lightning: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Particles/Particles.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Particles/Particles.vue?raw';
import type { CodeObject } from '../../../types/code';
export const particles: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Particles`,
@@ -36,4 +36,4 @@ export const particles: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Silk/Silk.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Silk/Silk.vue?raw';
import type { CodeObject } from '../../../types/code';
export const silk: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Silk`,
@@ -30,4 +30,4 @@ export const silk: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Squares/Squares.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Squares/Squares.vue?raw';
import type { CodeObject } from '../../../types/code';
export const squares: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Squares`,
@@ -19,4 +19,4 @@ export const squares: CodeObject = {
import Squares from "./Squares.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Threads/Threads.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Threads/Threads.vue?raw';
import type { CodeObject } from '../../../types/code';
export const threads: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Threads`,
@@ -19,4 +19,4 @@ export const threads: CodeObject = {
import Threads from "./Threads.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Waves/Waves.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Waves/Waves.vue?raw';
import type { CodeObject } from '../../../types/code';
export const waves: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Waves`,
@@ -35,4 +35,4 @@ export const waves: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/CardSwap/CardSwap.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/CardSwap/CardSwap.vue?raw';
import type { CodeObject } from '../../../types/code';
export const cardSwap: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/CardSwap`,
@@ -51,4 +51,4 @@ export const cardSwap: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/Carousel/Carousel.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/Carousel/Carousel.vue?raw';
import type { CodeObject } from '../../../types/code';
export const carousel: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/Carousel`,
@@ -37,4 +37,4 @@ export const carousel: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/CircularGallery/CircularGallery.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/CircularGallery/CircularGallery.vue?raw';
import type { CodeObject } from '../../../types/code';
export const circularGallery: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/CircularGallery`,
@@ -24,4 +24,4 @@ export const circularGallery: CodeObject = {
import CircularGallery from "./CircularGallery.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/DecayCard/DecayCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/DecayCard/DecayCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const decayCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/DecayCard`,
@@ -20,4 +20,4 @@ export const decayCard: CodeObject = {
import DecayCard from "./DecayCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/Dock/Dock.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/Dock/Dock.vue?raw';
import type { CodeObject } from '../../../types/code';
export const dock: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/Dock`,
@@ -44,4 +44,4 @@ export const dock: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/ElasticSlider/ElasticSlider.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/ElasticSlider/ElasticSlider.vue?raw';
import type { CodeObject } from '../../../types/code';
export const elasticSlider: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/ElasticSlider`,
@@ -27,4 +27,4 @@ export const elasticSlider: CodeObject = {
import ElasticSlider from "./ElasticSlider.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/FlowingMenu/FlowingMenu.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/FlowingMenu/FlowingMenu.vue?raw';
import type { CodeObject } from '../../../types/code';
export const flowingMenu: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/FlowingMenu`,
@@ -19,4 +19,4 @@ export const flowingMenu: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/FlyingPosters/FlyingPosters.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/FlyingPosters/FlyingPosters.vue?raw';
import type { CodeObject } from '../../../types/code';
export const flyingPosters: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/FlyingPosters`,
@@ -34,4 +34,4 @@ export const flyingPosters: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/GlassIcons/GlassIcons.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/GlassIcons/GlassIcons.vue?raw';
import type { CodeObject } from '../../../types/code';
export const glassIcons: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/GlassIcons`,
@@ -20,4 +20,4 @@ export const glassIcons: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/GooeyNav/GooeyNav.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/GooeyNav/GooeyNav.vue?raw';
import type { CodeObject } from '../../../types/code';
export const gooeyNav: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/GooeyNav`,
@@ -37,4 +37,4 @@ export const gooeyNav: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/InfiniteScroll/InfiniteScroll.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/InfiniteScroll/InfiniteScroll.vue?raw';
import type { CodeObject } from '../../../types/code';
export const infiniteScroll: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/InfiniteScroll`,
@@ -31,4 +31,4 @@ export const infiniteScroll: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/Masonry/Masonry.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/Masonry/Masonry.vue?raw';
import type { CodeObject } from '../../../types/code';
export const masonry: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/Masonry`,
@@ -29,4 +29,4 @@ const items = ref([
])
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/PixelCard/PixelCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/PixelCard/PixelCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const pixelCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/PixelCard`,
@@ -18,4 +18,4 @@ export const pixelCard: CodeObject = {
import PixelCard from "./PixelCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/ProfileCard/ProfileCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/ProfileCard/ProfileCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const profileCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/ProfileCard`,
@@ -28,4 +28,4 @@ export const profileCard: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/SpotlightCard/SpotlightCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/SpotlightCard/SpotlightCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const spotlightCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/SpotlightCard`,
@@ -16,4 +16,4 @@ export const spotlightCard: CodeObject = {
import SpotlightCard from "./SpotlightCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/TiltedCard/TiltedCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/TiltedCard/TiltedCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const tiltedCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/TiltedCard`,
@@ -31,4 +31,4 @@ export const tiltedCard: CodeObject = {
import TiltedCard from "./TiltedCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/BlurText/BlurText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/BlurText/BlurText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const blurText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/BlurText`,
@@ -26,4 +26,4 @@ export const blurText: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/CircularText/CircularText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/CircularText/CircularText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const circularText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/CircularText`,
@@ -17,4 +17,4 @@ export const circularText: CodeObject = {
import CircularText from "./CircularText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/CurvedLoop/CurvedLoop.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/CurvedLoop/CurvedLoop.vue?raw';
import type { CodeObject } from '../../../types/code';
export const curvedLoop: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/CurvedLoop`,
@@ -17,4 +17,4 @@ export const curvedLoop: CodeObject = {
import CurvedLoop from "./CurvedLoop.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/DecryptedText/DecryptedText.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/DecryptedText/DecryptedText.vue?raw';
import type { CodeObject } from '@/types/code';
export const decryptedText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/DecryptedText`,
@@ -21,4 +21,4 @@ export const decryptedText: CodeObject = {
import DecryptedText from "./DecryptedText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/FallingText/FallingText.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/FallingText/FallingText.vue?raw';
import type { CodeObject } from '@/types/code';
export const fallingText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/FallingText`,
@@ -19,4 +19,4 @@ export const fallingText: CodeObject = {
import FallingText from "./FallingText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/FuzzyText/FuzzyText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/FuzzyText/FuzzyText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const fuzzyText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/FuzzyText`,
@@ -19,4 +19,4 @@ export const fuzzyText: CodeObject = {
import FuzzyText from "./FuzzyText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const gradientText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/GradientText`,
@@ -17,4 +17,4 @@ export const gradientText: CodeObject = {
import GradientText from "./GradientText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/ScrollFloat/ScrollFloat.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/ScrollFloat/ScrollFloat.vue?raw';
import type { CodeObject } from '../../../types/code';
export const scrollFloatCode: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/ScrollFloat`,
@@ -22,4 +22,4 @@ export const scrollFloatCode: CodeObject = {
import ScrollFloat from "./ScrollFloat.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/ShinyText/ShinyText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/ShinyText/ShinyText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const shinyText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/ShinyText`,
@@ -16,4 +16,4 @@ export const shinyText: CodeObject = {
import ShinyText from "./ShinyText.vue";
</script>`,
code
}
};

View File

@@ -1,6 +1,6 @@
// Fun fact: this is the first component ever made for Vue Bits!
import code from '@content/TextAnimations/SplitText/SplitText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/SplitText/SplitText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const splitText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/SplitText`,
@@ -30,4 +30,4 @@ export const splitText: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/TextCursor/TextCursor.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/TextCursor/TextCursor.vue?raw';
import type { CodeObject } from '@/types/code';
export const textCursor: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TextCursor`,
@@ -21,4 +21,4 @@ export const textCursor: CodeObject = {
import TextCursor from "./TextCursor.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/TextPressure/TextPressure.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/TextPressure/TextPressure.vue?raw';
import type { CodeObject } from '../../../types/code';
export const textPressure: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TextPressure`,
@@ -22,4 +22,4 @@ export const textPressure: CodeObject = {
import TextPressure from "./TextPressure.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/TextTrail/TextTrail.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/TextTrail/TextTrail.vue?raw';
import type { CodeObject } from '@/types/code';
export const textTrail: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TextTrail`,
@@ -20,4 +20,4 @@ export const textTrail: CodeObject = {
import TextTrail from "./TextTrail.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from "@/content/TextAnimations/TrueFocus/TrueFocus.vue?raw";
import type { CodeObject } from "../../../types/code";
import code from '@/content/TextAnimations/TrueFocus/TrueFocus.vue?raw';
import type { CodeObject } from '../../../types/code';
export const trueFocus: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TrueFocus`,
@@ -18,5 +18,5 @@ export const trueFocus: CodeObject = {
<script setup lang="ts">
import TrueFocus from "./TrueFocus.vue";
</script>`,
code,
code
};

View File

@@ -1,22 +1,22 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(ScrollTrigger);
interface AnimatedContentProps {
distance?: number
direction?: 'vertical' | 'horizontal'
reverse?: boolean
duration?: number
ease?: string | ((progress: number) => number)
initialOpacity?: number
animateOpacity?: boolean
scale?: number
threshold?: number
delay?: number
className?: string
distance?: number;
direction?: 'vertical' | 'horizontal';
reverse?: boolean;
duration?: number;
ease?: string | ((progress: number) => number);
initialOpacity?: number;
animateOpacity?: boolean;
scale?: number;
threshold?: number;
delay?: number;
className?: string;
}
const props = withDefaults(defineProps<AnimatedContentProps>(), {
@@ -31,27 +31,27 @@ const props = withDefaults(defineProps<AnimatedContentProps>(), {
threshold: 0.1,
delay: 0,
className: ''
})
});
const emit = defineEmits<{
complete: []
}>()
complete: [];
}>();
const containerRef = ref<HTMLDivElement>()
const containerRef = ref<HTMLDivElement>();
onMounted(() => {
const el = containerRef.value
if (!el) return
const el = containerRef.value;
if (!el) return;
const axis = props.direction === 'horizontal' ? 'x' : 'y'
const offset = props.reverse ? -props.distance : props.distance
const startPct = (1 - props.threshold) * 100
const axis = props.direction === 'horizontal' ? 'x' : 'y';
const offset = props.reverse ? -props.distance : props.distance;
const startPct = (1 - props.threshold) * 100;
gsap.set(el, {
[axis]: offset,
scale: props.scale,
opacity: props.animateOpacity ? props.initialOpacity : 1,
})
opacity: props.animateOpacity ? props.initialOpacity : 1
});
gsap.to(el, {
[axis]: 0,
@@ -65,10 +65,10 @@ onMounted(() => {
trigger: el,
start: `top ${startPct}%`,
toggleActions: 'play none none none',
once: true,
},
})
})
once: true
}
});
});
watch(
() => [
@@ -81,24 +81,24 @@ watch(
props.animateOpacity,
props.scale,
props.threshold,
props.delay,
props.delay
],
() => {
const el = containerRef.value
if (!el) return
const el = containerRef.value;
if (!el) return;
ScrollTrigger.getAll().forEach((t) => t.kill())
gsap.killTweensOf(el)
ScrollTrigger.getAll().forEach(t => t.kill());
gsap.killTweensOf(el);
const axis = props.direction === 'horizontal' ? 'x' : 'y'
const offset = props.reverse ? -props.distance : props.distance
const startPct = (1 - props.threshold) * 100
const axis = props.direction === 'horizontal' ? 'x' : 'y';
const offset = props.reverse ? -props.distance : props.distance;
const startPct = (1 - props.threshold) * 100;
gsap.set(el, {
[axis]: offset,
scale: props.scale,
opacity: props.animateOpacity ? props.initialOpacity : 1,
})
opacity: props.animateOpacity ? props.initialOpacity : 1
});
gsap.to(el, {
[axis]: 0,
@@ -112,27 +112,24 @@ watch(
trigger: el,
start: `top ${startPct}%`,
toggleActions: 'play none none none',
once: true,
},
})
once: true
}
});
},
{ deep: true }
)
);
onUnmounted(() => {
const el = containerRef.value
const el = containerRef.value;
if (el) {
ScrollTrigger.getAll().forEach((t) => t.kill())
gsap.killTweensOf(el)
ScrollTrigger.getAll().forEach(t => t.kill());
gsap.killTweensOf(el);
}
})
});
</script>
<template>
<div
ref="containerRef"
:class="`animated-content ${props.className}`"
>
<div ref="containerRef" :class="`animated-content ${props.className}`">
<slot />
</div>
</template>

View File

@@ -1,35 +1,29 @@
<template>
<div
ref="containerRef"
class="relative w-full h-full"
@click="handleClick"
>
<canvas
ref="canvasRef"
class="absolute inset-0 pointer-events-none"
/>
<div ref="containerRef" class="relative w-full h-full" @click="handleClick">
<canvas ref="canvasRef" class="absolute inset-0 pointer-events-none" />
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
interface Spark {
x: number
y: number
angle: number
startTime: number
x: number;
y: number;
angle: number;
startTime: number;
}
interface Props {
sparkColor?: string
sparkSize?: number
sparkRadius?: number
sparkCount?: number
duration?: number
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out"
extraScale?: number
sparkColor?: string;
sparkSize?: number;
sparkRadius?: number;
sparkCount?: number;
duration?: number;
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
extraScale?: number;
}
const props = withDefaults(defineProps<Props>(), {
@@ -40,138 +34,139 @@ const props = withDefaults(defineProps<Props>(), {
duration: 400,
easing: 'ease-out',
extraScale: 1.0
})
});
const containerRef = ref<HTMLDivElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
const sparks = ref<Spark[]>([])
const startTimeRef = ref<number | null>(null)
const animationId = ref<number | null>(null)
const containerRef = ref<HTMLDivElement | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const sparks = ref<Spark[]>([]);
const startTimeRef = ref<number | null>(null);
const animationId = ref<number | null>(null);
const easeFunc = computed(() => {
return (t: number) => {
switch (props.easing) {
case "linear":
return t
case "ease-in":
return t * t
case "ease-in-out":
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
case 'linear':
return t;
case 'ease-in':
return t * t;
case 'ease-in-out':
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
default:
return t * (2 - t)
return t * (2 - t);
}
}
})
};
});
const handleClick = (e: MouseEvent) => {
const canvas = canvasRef.value
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const canvas = canvasRef.value;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const now = performance.now()
const now = performance.now();
const newSparks: Spark[] = Array.from({ length: props.sparkCount }, (_, i) => ({
x,
y,
angle: (2 * Math.PI * i) / props.sparkCount,
startTime: now,
}))
startTime: now
}));
sparks.value.push(...newSparks)
}
sparks.value.push(...newSparks);
};
const draw = (timestamp: number) => {
if (!startTimeRef.value) {
startTimeRef.value = timestamp
startTimeRef.value = timestamp;
}
const canvas = canvasRef.value
const ctx = canvas?.getContext('2d')
if (!ctx || !canvas) return
const canvas = canvasRef.value;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.clearRect(0, 0, canvas.width, canvas.height);
sparks.value = sparks.value.filter((spark: Spark) => {
const elapsed = timestamp - spark.startTime
const elapsed = timestamp - spark.startTime;
if (elapsed >= props.duration) {
return false
return false;
}
const progress = elapsed / props.duration
const eased = easeFunc.value(progress)
const progress = elapsed / props.duration;
const eased = easeFunc.value(progress);
const distance = eased * props.sparkRadius * props.extraScale
const lineLength = props.sparkSize * (1 - eased)
const distance = eased * props.sparkRadius * props.extraScale;
const lineLength = props.sparkSize * (1 - eased);
const x1 = spark.x + distance * Math.cos(spark.angle)
const y1 = spark.y + distance * Math.sin(spark.angle)
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle)
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle)
const x1 = spark.x + distance * Math.cos(spark.angle);
const y1 = spark.y + distance * Math.sin(spark.angle);
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle);
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle);
ctx.strokeStyle = props.sparkColor
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
ctx.strokeStyle = props.sparkColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
return true
})
return true;
});
animationId.value = requestAnimationFrame(draw)
}
animationId.value = requestAnimationFrame(draw);
};
const resizeCanvas = () => {
const canvas = canvasRef.value
if (!canvas) return
const canvas = canvasRef.value;
if (!canvas) return;
const parent = canvas.parentElement
if (!parent) return
const parent = canvas.parentElement;
if (!parent) return;
const { width, height } = parent.getBoundingClientRect()
const { width, height } = parent.getBoundingClientRect();
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
}
canvas.width = width;
canvas.height = height;
}
};
let resizeTimeout: number
let resizeTimeout: number;
const handleResize = () => {
clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(resizeCanvas, 100)
}
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizeCanvas, 100);
};
let resizeObserver: ResizeObserver | null = null
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
const canvas = canvasRef.value;
if (!canvas) return;
const parent = canvas.parentElement
if (!parent) return
const parent = canvas.parentElement;
if (!parent) return;
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(parent)
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(parent);
resizeCanvas()
resizeCanvas();
animationId.value = requestAnimationFrame(draw)
})
animationId.value = requestAnimationFrame(draw);
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver.disconnect();
}
clearTimeout(resizeTimeout)
clearTimeout(resizeTimeout);
if (animationId.value) {
cancelAnimationFrame(animationId.value)
cancelAnimationFrame(animationId.value);
}
})
});
watch([
watch(
[
() => props.sparkColor,
() => props.sparkSize,
() => props.sparkRadius,
@@ -179,10 +174,12 @@ watch([
() => props.duration,
easeFunc,
() => props.extraScale
], () => {
],
() => {
if (animationId.value) {
cancelAnimationFrame(animationId.value)
cancelAnimationFrame(animationId.value);
}
animationId.value = requestAnimationFrame(draw)
})
animationId.value = requestAnimationFrame(draw);
}
);
</script>

View File

@@ -3,161 +3,164 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
interface Props {
to: number
from?: number
direction?: "up" | "down"
delay?: number
duration?: number
className?: string
startWhen?: boolean
separator?: string
onStart?: () => void
onEnd?: () => void
to: number;
from?: number;
direction?: 'up' | 'down';
delay?: number;
duration?: number;
className?: string;
startWhen?: boolean;
separator?: string;
onStart?: () => void;
onEnd?: () => void;
}
const props = withDefaults(defineProps<Props>(), {
from: 0,
direction: "up",
direction: 'up',
delay: 0,
duration: 2,
className: "",
className: '',
startWhen: true,
separator: ""
})
separator: ''
});
const elementRef = ref<HTMLSpanElement | null>(null)
const currentValue = ref(props.direction === "down" ? props.to : props.from)
const isInView = ref(false)
const animationId = ref<number | null>(null)
const hasStarted = ref(false)
const elementRef = ref<HTMLSpanElement | null>(null);
const currentValue = ref(props.direction === 'down' ? props.to : props.from);
const isInView = ref(false);
const animationId = ref<number | null>(null);
const hasStarted = ref(false);
let intersectionObserver: IntersectionObserver | null = null
let intersectionObserver: IntersectionObserver | null = null;
const damping = computed(() => 20 + 40 * (1 / props.duration))
const stiffness = computed(() => 100 * (1 / props.duration))
const damping = computed(() => 20 + 40 * (1 / props.duration));
const stiffness = computed(() => 100 * (1 / props.duration));
let velocity = 0
let startTime = 0
let velocity = 0;
let startTime = 0;
const formatNumber = (value: number) => {
const options = {
useGrouping: !!props.separator,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}
maximumFractionDigits: 0
};
const formattedNumber = Intl.NumberFormat("en-US", options).format(
Number(value.toFixed(0))
)
const formattedNumber = Intl.NumberFormat('en-US', options).format(Number(value.toFixed(0)));
return props.separator
? formattedNumber.replace(/,/g, props.separator)
: formattedNumber
}
return props.separator ? formattedNumber.replace(/,/g, props.separator) : formattedNumber;
};
const updateDisplay = () => {
if (elementRef.value) {
elementRef.value.textContent = formatNumber(currentValue.value)
}
elementRef.value.textContent = formatNumber(currentValue.value);
}
};
const springAnimation = (timestamp: number) => {
if (!startTime) startTime = timestamp
if (!startTime) startTime = timestamp;
const target = props.direction === "down" ? props.from : props.to
const current = currentValue.value
const target = props.direction === 'down' ? props.from : props.to;
const current = currentValue.value;
const displacement = target - current
const springForce = displacement * stiffness.value
const dampingForce = velocity * damping.value
const acceleration = springForce - dampingForce
const displacement = target - current;
const springForce = displacement * stiffness.value;
const dampingForce = velocity * damping.value;
const acceleration = springForce - dampingForce;
velocity += acceleration * 0.016 // Assuming 60fps
currentValue.value += velocity * 0.016
velocity += acceleration * 0.016; // Assuming 60fps
currentValue.value += velocity * 0.016;
updateDisplay()
updateDisplay();
if (Math.abs(displacement) > 0.01 || Math.abs(velocity) > 0.01) {
animationId.value = requestAnimationFrame(springAnimation)
animationId.value = requestAnimationFrame(springAnimation);
} else {
currentValue.value = target
updateDisplay()
animationId.value = null
currentValue.value = target;
updateDisplay();
animationId.value = null;
if (props.onEnd) {
props.onEnd()
}
props.onEnd();
}
}
};
const startAnimation = () => {
if (hasStarted.value || !isInView.value || !props.startWhen) return
if (hasStarted.value || !isInView.value || !props.startWhen) return;
hasStarted.value = true
hasStarted.value = true;
if (props.onStart) {
props.onStart()
props.onStart();
}
setTimeout(() => {
startTime = 0
velocity = 0
animationId.value = requestAnimationFrame(springAnimation)
}, props.delay * 1000)
}
startTime = 0;
velocity = 0;
animationId.value = requestAnimationFrame(springAnimation);
}, props.delay * 1000);
};
const setupIntersectionObserver = () => {
if (!elementRef.value) return
if (!elementRef.value) return;
intersectionObserver = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isInView.value) {
isInView.value = true
startAnimation()
isInView.value = true;
startAnimation();
}
},
{
threshold: 0,
rootMargin: "0px"
rootMargin: '0px'
}
)
);
intersectionObserver.observe(elementRef.value)
}
intersectionObserver.observe(elementRef.value);
};
const cleanup = () => {
if (animationId.value) {
cancelAnimationFrame(animationId.value)
animationId.value = null
cancelAnimationFrame(animationId.value);
animationId.value = null;
}
if (intersectionObserver) {
intersectionObserver.disconnect()
intersectionObserver = null
}
intersectionObserver.disconnect();
intersectionObserver = null;
}
};
watch([() => props.from, () => props.to, () => props.direction], () => {
currentValue.value = props.direction === "down" ? props.to : props.from
updateDisplay()
hasStarted.value = false
}, { immediate: true })
watch(
[() => props.from, () => props.to, () => props.direction],
() => {
currentValue.value = props.direction === 'down' ? props.to : props.from;
updateDisplay();
hasStarted.value = false;
},
{ immediate: true }
);
watch(() => props.startWhen, () => {
watch(
() => props.startWhen,
() => {
if (props.startWhen && isInView.value && !hasStarted.value) {
startAnimation()
startAnimation();
}
})
}
);
onMounted(() => {
updateDisplay()
setupIntersectionObserver()
})
updateDisplay();
setupIntersectionObserver();
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
</script>

View File

@@ -2,46 +2,74 @@
<div class="relative w-1/2 max-md:w-11/12 aspect-square" :style="wrapperStyle">
<div ref="sceneRef" class="grid w-full h-full" :style="sceneStyle">
<template v-for="(_, r) in cells" :key="`row-${r}`">
<div v-for="(__, c) in cells" :key="`${r}-${c}`"
class="cube relative w-full h-full aspect-square [transform-style:preserve-3d]" :data-row="r" :data-col="c">
<div
v-for="(__, c) in cells"
:key="`${r}-${c}`"
class="cube relative w-full h-full aspect-square [transform-style:preserve-3d]"
:data-row="r"
:data-col="c"
>
<span class="absolute pointer-events-none -inset-9" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateY(-50%) rotateX(90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
transform: 'translateY(-50%) rotateX(90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateY(50%) rotateX(-90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
transform: 'translateY(50%) rotateX(-90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateX(-50%) rotateY(-90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
transform: 'translateX(-50%) rotateY(-90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateX(50%) rotateY(90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
transform: 'translateX(50%) rotateY(90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'rotateY(-90deg) translateX(50%) rotateY(90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
transform: 'rotateY(-90deg) translateX(50%) rotateY(90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'rotateY(90deg) translateX(-50%) rotateY(-90deg)',
}" />
transform: 'rotateY(90deg) translateX(-50%) rotateY(-90deg)'
}"
/>
</div>
</template>
</div>
@@ -49,34 +77,34 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, withDefaults } from 'vue'
import gsap from 'gsap'
import { ref, computed, onMounted, onUnmounted, withDefaults } from 'vue';
import gsap from 'gsap';
interface Gap {
row: number
col: number
row: number;
col: number;
}
interface Duration {
enter: number
leave: number
enter: number;
leave: number;
}
interface Props {
gridSize?: number
cubeSize?: number
maxAngle?: number
radius?: number
easing?: gsap.EaseString
duration?: Duration
cellGap?: number | Gap
borderStyle?: string
faceColor?: string
shadow?: boolean | string
autoAnimate?: boolean
rippleOnClick?: boolean
rippleColor?: string
rippleSpeed?: number
gridSize?: number;
cubeSize?: number;
maxAngle?: number;
radius?: number;
easing?: gsap.EaseString;
duration?: Duration;
cellGap?: number | Gap;
borderStyle?: string;
faceColor?: string;
shadow?: boolean | string;
autoAnimate?: boolean;
rippleOnClick?: boolean;
rippleColor?: string;
rippleSpeed?: number;
}
const props = withDefaults(defineProps<Props>(), {
@@ -91,37 +119,37 @@ const props = withDefaults(defineProps<Props>(), {
autoAnimate: true,
rippleOnClick: true,
rippleColor: '#fff',
rippleSpeed: 2,
})
rippleSpeed: 2
});
const sceneRef = ref<HTMLDivElement | null>(null)
const rafRef = ref<number | null>(null)
const idleTimerRef = ref<number | null>(null)
const userActiveRef = ref(false)
const simPosRef = ref<{ x: number; y: number }>({ x: 0, y: 0 })
const simTargetRef = ref<{ x: number; y: number }>({ x: 0, y: 0 })
const simRAFRef = ref<number | null>(null)
const sceneRef = ref<HTMLDivElement | null>(null);
const rafRef = ref<number | null>(null);
const idleTimerRef = ref<number | null>(null);
const userActiveRef = ref(false);
const simPosRef = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const simTargetRef = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const simRAFRef = ref<number | null>(null);
const colGap = computed(() => {
return typeof props.cellGap === 'number'
? `${props.cellGap}px`
: (props.cellGap as Gap)?.col !== undefined
? `${(props.cellGap as Gap).col}px`
: '5%'
})
: '5%';
});
const rowGap = computed(() => {
return typeof props.cellGap === 'number'
? `${props.cellGap}px`
: (props.cellGap as Gap)?.row !== undefined
? `${(props.cellGap as Gap).row}px`
: '5%'
})
: '5%';
});
const enterDur = computed(() => props.duration.enter)
const leaveDur = computed(() => props.duration.leave)
const enterDur = computed(() => props.duration.enter);
const leaveDur = computed(() => props.duration.leave);
const cells = computed(() => Array.from({ length: props.gridSize }))
const cells = computed(() => Array.from({ length: props.gridSize }));
const sceneStyle = computed(() => ({
gridTemplateColumns: props.cubeSize
@@ -133,189 +161,184 @@ const sceneStyle = computed(() => ({
columnGap: colGap.value,
rowGap: rowGap.value,
perspective: '99999999px',
gridAutoRows: '1fr',
}))
gridAutoRows: '1fr'
}));
const wrapperStyle = computed(() => ({
'--cube-face-border': props.borderStyle,
'--cube-face-bg': props.faceColor,
'--cube-face-shadow':
props.shadow === true ? '0 0 6px rgba(0,0,0,.5)' : props.shadow || 'none',
'--cube-face-shadow': props.shadow === true ? '0 0 6px rgba(0,0,0,.5)' : props.shadow || 'none',
...(props.cubeSize
? {
width: `${props.gridSize * props.cubeSize}px`,
height: `${props.gridSize * props.cubeSize}px`,
height: `${props.gridSize * props.cubeSize}px`
}
: {}),
}))
: {})
}));
const tiltAt = (rowCenter: number, colCenter: number) => {
if (!sceneRef.value) return
if (!sceneRef.value) return;
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) => {
const r = +(cube.dataset.row!)
const c = +(cube.dataset.col!)
const dist = Math.hypot(r - rowCenter, c - colCenter)
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube => {
const r = +cube.dataset.row!;
const c = +cube.dataset.col!;
const dist = Math.hypot(r - rowCenter, c - colCenter);
if (dist <= props.radius) {
const pct = 1 - dist / props.radius
const angle = pct * props.maxAngle
const pct = 1 - dist / props.radius;
const angle = pct * props.maxAngle;
gsap.to(cube, {
duration: enterDur.value,
ease: props.easing,
overwrite: true,
rotateX: -angle,
rotateY: angle,
})
rotateY: angle
});
} else {
gsap.to(cube, {
duration: leaveDur.value,
ease: 'power3.out',
overwrite: true,
rotateX: 0,
rotateY: 0,
})
}
})
rotateY: 0
});
}
});
};
const onPointerMove = (e: PointerEvent) => {
userActiveRef.value = true
if (idleTimerRef.value) clearTimeout(idleTimerRef.value)
userActiveRef.value = true;
if (idleTimerRef.value) clearTimeout(idleTimerRef.value);
const rect = sceneRef.value!.getBoundingClientRect()
const cellW = rect.width / props.gridSize
const cellH = rect.height / props.gridSize
const colCenter = (e.clientX - rect.left) / cellW
const rowCenter = (e.clientY - rect.top) / cellH
const rect = sceneRef.value!.getBoundingClientRect();
const cellW = rect.width / props.gridSize;
const cellH = rect.height / props.gridSize;
const colCenter = (e.clientX - rect.left) / cellW;
const rowCenter = (e.clientY - rect.top) / cellH;
if (rafRef.value) cancelAnimationFrame(rafRef.value)
rafRef.value = requestAnimationFrame(() =>
tiltAt(rowCenter, colCenter)
)
if (rafRef.value) cancelAnimationFrame(rafRef.value);
rafRef.value = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));
idleTimerRef.value = setTimeout(() => {
userActiveRef.value = false
}, 3000)
}
userActiveRef.value = false;
}, 3000);
};
const resetAll = () => {
if (!sceneRef.value) return
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) =>
if (!sceneRef.value) return;
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube =>
gsap.to(cube, {
duration: leaveDur.value,
rotateX: 0,
rotateY: 0,
ease: 'power3.out',
ease: 'power3.out'
})
)
}
);
};
const onClick = (e: MouseEvent) => {
if (!props.rippleOnClick || !sceneRef.value) return
if (!props.rippleOnClick || !sceneRef.value) return;
const rect = sceneRef.value.getBoundingClientRect()
const cellW = rect.width / props.gridSize
const cellH = rect.height / props.gridSize
const colHit = Math.floor((e.clientX - rect.left) / cellW)
const rowHit = Math.floor((e.clientY - rect.top) / cellH)
const rect = sceneRef.value.getBoundingClientRect();
const cellW = rect.width / props.gridSize;
const cellH = rect.height / props.gridSize;
const colHit = Math.floor((e.clientX - rect.left) / cellW);
const rowHit = Math.floor((e.clientY - rect.top) / cellH);
const baseRingDelay = 0.15
const baseAnimDur = 0.3
const baseHold = 0.6
const baseRingDelay = 0.15;
const baseAnimDur = 0.3;
const baseHold = 0.6;
const spreadDelay = baseRingDelay / props.rippleSpeed
const animDuration = baseAnimDur / props.rippleSpeed
const holdTime = baseHold / props.rippleSpeed
const spreadDelay = baseRingDelay / props.rippleSpeed;
const animDuration = baseAnimDur / props.rippleSpeed;
const holdTime = baseHold / props.rippleSpeed;
const rings: Record<number, HTMLDivElement[]> = {}
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) => {
const r = +(cube.dataset.row!)
const c = +(cube.dataset.col!)
const dist = Math.hypot(r - rowHit, c - colHit)
const ring = Math.round(dist)
if (!rings[ring]) rings[ring] = []
rings[ring].push(cube)
})
const rings: Record<number, HTMLDivElement[]> = {};
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube => {
const r = +cube.dataset.row!;
const c = +cube.dataset.col!;
const dist = Math.hypot(r - rowHit, c - colHit);
const ring = Math.round(dist);
if (!rings[ring]) rings[ring] = [];
rings[ring].push(cube);
});
Object.keys(rings)
.map(Number)
.sort((a, b) => a - b)
.forEach((ring) => {
const delay = ring * spreadDelay
const faces = rings[ring].flatMap((cube) =>
Array.from(cube.querySelectorAll<HTMLElement>('.cube-face'))
)
.forEach(ring => {
const delay = ring * spreadDelay;
const faces = rings[ring].flatMap(cube => Array.from(cube.querySelectorAll<HTMLElement>('.cube-face')));
gsap.to(faces, {
backgroundColor: props.rippleColor,
duration: animDuration,
delay,
ease: 'power3.out',
})
ease: 'power3.out'
});
gsap.to(faces, {
backgroundColor: props.faceColor,
duration: animDuration,
delay: delay + animDuration + holdTime,
ease: 'power3.out',
})
})
}
ease: 'power3.out'
});
});
};
const startAutoAnimation = () => {
if (!props.autoAnimate || !sceneRef.value) return
if (!props.autoAnimate || !sceneRef.value) return;
simPosRef.value = {
x: Math.random() * props.gridSize,
y: Math.random() * props.gridSize,
}
y: Math.random() * props.gridSize
};
simTargetRef.value = {
x: Math.random() * props.gridSize,
y: Math.random() * props.gridSize,
}
y: Math.random() * props.gridSize
};
const speed = 0.02
const speed = 0.02;
const loop = () => {
if (!userActiveRef.value) {
const pos = simPosRef.value
const tgt = simTargetRef.value
pos.x += (tgt.x - pos.x) * speed
pos.y += (tgt.y - pos.y) * speed
tiltAt(pos.y, pos.x)
const pos = simPosRef.value;
const tgt = simTargetRef.value;
pos.x += (tgt.x - pos.x) * speed;
pos.y += (tgt.y - pos.y) * speed;
tiltAt(pos.y, pos.x);
if (Math.hypot(pos.x - tgt.x, pos.y - tgt.y) < 0.1) {
simTargetRef.value = {
x: Math.random() * props.gridSize,
y: Math.random() * props.gridSize,
y: Math.random() * props.gridSize
};
}
}
}
simRAFRef.value = requestAnimationFrame(loop)
}
simRAFRef.value = requestAnimationFrame(loop)
}
simRAFRef.value = requestAnimationFrame(loop);
};
simRAFRef.value = requestAnimationFrame(loop);
};
onMounted(() => {
const el = sceneRef.value
if (!el) return
const el = sceneRef.value;
if (!el) return;
el.addEventListener('pointermove', onPointerMove)
el.addEventListener('pointerleave', resetAll)
el.addEventListener('click', onClick)
el.addEventListener('pointermove', onPointerMove);
el.addEventListener('pointerleave', resetAll);
el.addEventListener('click', onClick);
startAutoAnimation()
})
startAutoAnimation();
});
onUnmounted(() => {
const el = sceneRef.value
const el = sceneRef.value;
if (el) {
el.removeEventListener('pointermove', onPointerMove)
el.removeEventListener('pointerleave', resetAll)
el.removeEventListener('click', onClick)
el.removeEventListener('pointermove', onPointerMove);
el.removeEventListener('pointerleave', resetAll);
el.removeEventListener('click', onClick);
}
if (rafRef.value !== null) cancelAnimationFrame(rafRef.value)
if (idleTimerRef.value !== null) clearTimeout(idleTimerRef.value)
if (simRAFRef.value !== null) cancelAnimationFrame(simRAFRef.value)
})
if (rafRef.value !== null) cancelAnimationFrame(rafRef.value);
if (idleTimerRef.value !== null) clearTimeout(idleTimerRef.value);
if (simRAFRef.value !== null) cancelAnimationFrame(simRAFRef.value);
});
</script>

View File

@@ -5,7 +5,7 @@
:style="{
opacity: inView ? 1 : initialOpacity,
transition: `opacity ${duration}ms ${easing}, filter ${duration}ms ${easing}`,
filter: blur ? (inView ? 'blur(0px)' : 'blur(10px)') : 'none',
filter: blur ? (inView ? 'blur(0px)' : 'blur(10px)') : 'none'
}"
>
<slot />
@@ -13,16 +13,16 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue';
interface Props {
blur?: boolean
duration?: number
easing?: string
delay?: number
threshold?: number
initialOpacity?: number
className?: string
blur?: boolean;
duration?: number;
easing?: string;
delay?: number;
threshold?: number;
initialOpacity?: number;
className?: string;
}
const props = withDefaults(defineProps<Props>(), {
@@ -33,34 +33,34 @@ const props = withDefaults(defineProps<Props>(), {
threshold: 0.1,
initialOpacity: 0,
className: ''
})
});
const inView = ref(false)
const elementRef = ref<HTMLDivElement | null>(null)
let observer: IntersectionObserver | null = null
const inView = ref(false);
const elementRef = ref<HTMLDivElement | null>(null);
let observer: IntersectionObserver | null = null;
onMounted(() => {
const element = elementRef.value
if (!element) return
const element = elementRef.value;
if (!element) return;
observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
observer?.unobserve(element)
observer?.unobserve(element);
setTimeout(() => {
inView.value = true
}, props.delay)
inView.value = true;
}, props.delay);
}
},
{ threshold: props.threshold }
)
);
observer.observe(element)
})
observer.observe(element);
});
onUnmounted(() => {
if (observer) {
observer.disconnect()
observer.disconnect();
}
})
});
</script>

View File

@@ -1,20 +1,20 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed } from 'vue';
interface GlareHoverProps {
width?: string
height?: string
background?: string
borderRadius?: string
borderColor?: string
glareColor?: string
glareOpacity?: number
glareAngle?: number
glareSize?: number
transitionDuration?: number
playOnce?: boolean
className?: string
style?: Record<string, string | number>
width?: string;
height?: string;
background?: string;
borderRadius?: string;
borderColor?: string;
glareColor?: string;
glareOpacity?: number;
glareAngle?: number;
glareSize?: number;
transitionDuration?: number;
playOnce?: boolean;
className?: string;
style?: Record<string, string | number>;
}
const props = withDefaults(defineProps<GlareHoverProps>(), {
@@ -31,28 +31,28 @@ const props = withDefaults(defineProps<GlareHoverProps>(), {
playOnce: false,
className: '',
style: () => ({})
})
});
const overlayRef = ref<HTMLDivElement | null>(null)
const overlayRef = ref<HTMLDivElement | null>(null);
const rgba = computed(() => {
const hex = props.glareColor.replace('#', '')
let result = props.glareColor
const hex = props.glareColor.replace('#', '');
let result = props.glareColor;
if (/^[\dA-Fa-f]{6}$/.test(hex)) {
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`;
} else if (/^[\dA-Fa-f]{3}$/.test(hex)) {
const r = parseInt(hex[0] + hex[0], 16)
const g = parseInt(hex[1] + hex[1], 16)
const b = parseInt(hex[2] + hex[2], 16)
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`
const r = parseInt(hex[0] + hex[0], 16);
const g = parseInt(hex[1] + hex[1], 16);
const b = parseInt(hex[2] + hex[2], 16);
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`;
}
return result
})
return result;
});
const overlayStyle = computed(() => ({
position: 'absolute' as const,
@@ -64,32 +64,32 @@ const overlayStyle = computed(() => ({
backgroundSize: `${props.glareSize}% ${props.glareSize}%, 100% 100%`,
backgroundRepeat: 'no-repeat',
backgroundPosition: '-100% -100%, 0 0',
pointerEvents: 'none' as const,
}))
pointerEvents: 'none' as const
}));
const animateIn = () => {
const el = overlayRef.value
if (!el) return
const el = overlayRef.value;
if (!el) return;
el.style.transition = 'none'
el.style.backgroundPosition = '-100% -100%, 0 0'
void el.offsetHeight
el.style.transition = `${props.transitionDuration}ms ease`
el.style.backgroundPosition = '100% 100%, 0 0'
}
el.style.transition = 'none';
el.style.backgroundPosition = '-100% -100%, 0 0';
void el.offsetHeight;
el.style.transition = `${props.transitionDuration}ms ease`;
el.style.backgroundPosition = '100% 100%, 0 0';
};
const animateOut = () => {
const el = overlayRef.value
if (!el) return
const el = overlayRef.value;
if (!el) return;
if (props.playOnce) {
el.style.transition = 'none'
el.style.backgroundPosition = '-100% -100%, 0 0'
el.style.transition = 'none';
el.style.backgroundPosition = '-100% -100%, 0 0';
} else {
el.style.transition = `${props.transitionDuration}ms ease`
el.style.backgroundPosition = '-100% -100%, 0 0'
}
el.style.transition = `${props.transitionDuration}ms ease`;
el.style.backgroundPosition = '-100% -100%, 0 0';
}
};
</script>
<template>
@@ -101,12 +101,13 @@ const animateOut = () => {
background: props.background,
borderRadius: props.borderRadius,
borderColor: props.borderColor,
...props.style,
...props.style
}"
@mouseenter="animateIn"
@mouseleave="animateOut"
>
<div ref="overlayRef" :style="overlayStyle" />
<slot />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More