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,47 +1,47 @@
name: 🐞 Bug report name: 🐞 Bug report
description: Help improve Vue Bits. description: Help improve Vue Bits.
labels: ["bug"] labels: ['bug']
title: "[BUG]: " title: '[BUG]: '
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
## Thanks for trying to improve Vue Bits! ## Thanks for trying to improve Vue Bits!
Before continuing make sure you have checked other issues to see if your issue has already been reported / addressed. Before continuing make sure you have checked other issues to see if your issue has already been reported / addressed.
- type: textarea - type: textarea
id: desc id: desc
attributes: attributes:
label: Describe the issue label: Describe the issue
description: What is happening right now? What is supposed to happen? description: What is happening right now? What is supposed to happen?
placeholder: When I do ..., it does ... but it should do ... placeholder: When I do ..., it does ... but it should do ...
validations: validations:
required: true required: true
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
## Reproduction ## Reproduction
Please provide code snippets/screenshots and, if possible/needed, a live environment where your bug can be reproduced. Please provide code snippets/screenshots and, if possible/needed, a live environment where your bug can be reproduced.
- type: input - type: input
id: reproduction-link id: reproduction-link
attributes: attributes:
label: Reproduction Link label: Reproduction Link
description: Link a live environment you used to reproduce. description: Link a live environment you used to reproduce.
placeholder: https://github.com/DavidHDev/vue-bits placeholder: https://github.com/DavidHDev/vue-bits
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: repro-steps id: repro-steps
attributes: attributes:
label: Steps to reproduce label: Steps to reproduce
description: What steps should be taken to reproduce your issue. description: What steps should be taken to reproduce your issue.
validations: validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Validations
description: Please make sure you have checked all of the following.
options:
- label: I have checked other issues to see if my issue was already reported or addressed
required: true required: true
- type: checkboxes
id: terms
attributes:
label: Validations
description: Please make sure you have checked all of the following.
options:
- label: I have checked other issues to see if my issue was already reported or addressed
required: true

View File

@@ -1,26 +1,26 @@
name: 💡 Feature Request name: 💡 Feature Request
description: Suggest something for Vue Bits. description: Suggest something for Vue Bits.
labels: ["enhancement"] labels: ['enhancement']
title: "[FEAT]: " title: '[FEAT]: '
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
## Thanks for trying to improve Vue Bits! ## Thanks for trying to improve Vue Bits!
Before continuing make sure you have checked other issues to see if your idea has already been discussed / addressed. Before continuing make sure you have checked other issues to see if your idea has already been discussed / addressed.
- type: textarea - type: textarea
id: desc id: desc
attributes: attributes:
label: Share your suggestion label: Share your suggestion
description: What would you like to see in Vue Bits? description: What would you like to see in Vue Bits?
placeholder: I want flying pigs in a component please placeholder: I want flying pigs in a component please
validations: validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Validations
description: Please make sure you have checked all of the following.
options:
- label: I have checked other issues to see if my issue was already discussed or addressed
required: true required: true
- type: checkboxes
id: terms
attributes:
label: Validations
description: Please make sure you have checked all of the following.
options:
- label: I have checked other issues to see if my issue was already discussed or addressed
required: true

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

@@ -28,9 +28,9 @@ Go to [vue-bits.dev](https://vue-bits.dev/) to view the documentation.
## About ## About
This is the official Vue port of [React Bits](https://reactbits.dev)! This is the official Vue port of [React Bits](https://reactbits.dev)!
Vue Bits is a large collection of animated Vue UI components made to spice up your web creations. We've got animations, components, backgrounds, and awesome stuff that you won't be able to find anywhere else - all free for you to use! Vue Bits is a large collection of animated Vue UI components made to spice up your web creations. We've got animations, components, backgrounds, and awesome stuff that you won't be able to find anywhere else - all free for you to use!
These components are all enhanced with customization options as props, to make it easy for you to get exactly what you need. These components are all enhanced with customization options as props, to make it easy for you to get exactly what you need.
@@ -62,7 +62,7 @@ Please review the [Contribution Guide](https://github.com/DavidHDev/vue-bits/blo
## Stats ## 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 ## Sponsorship

View File

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

View File

@@ -1,99 +1,107 @@
<!doctype html> <!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"
/>
<head> <!-- Font -->
<!-- Basic Meta Tags --> <link rel="preconnect" href="https://fonts.googleapis.com" />
<meta charset="UTF-8" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link
<meta name="theme-color" content="#0b0b0b" /> href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
<meta name="author" content="David Haz"> rel="stylesheet"
<meta name="description" />
content="An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces." /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<meta name="keywords" <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
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" /> <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" />
<!-- Font --> <!-- Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="icon" type="image/svg+xml" sizes="16x16 32x32" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96" />
<link <link rel="shortcut icon" href="favicon.ico" />
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
rel="stylesheet"> <meta name="apple-mobile-web-app-title" content="Vue Bits" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="manifest" href="/site.webmanifest" />
<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">
<!-- Icons --> <!-- Open Graph (OG) - Facebook, LinkedIn, etc. -->
<link rel="icon" type="image/svg+xml" sizes="16x16 32x32" href="favicon.ico" /> <meta property="og:type" content="website" />
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96" /> <meta property="og:title" content="Vue Bits" />
<link rel="shortcut icon" href="favicon.ico" /> <meta
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> property="og:description"
<meta name="apple-mobile-web-app-title" content="Vue Bits" /> content="An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces."
<link rel="manifest" href="/site.webmanifest" /> />
<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" />
<!-- Open Graph (OG) - Facebook, LinkedIn, etc. --> <!-- Twitter Card - Twitter Sharing -->
<meta property="og:type" content="website"> <meta name="twitter:card" content="summary_large_image" />
<meta property="og:title" content="Vue Bits"> <meta name="twitter:title" content="Vue Bits" />
<meta property="og:description" <meta
content="An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces."> name="twitter:description"
<meta property="og:image" content="https://vue-bits.dev/og-pic.png"> content="An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces."
<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 name="twitter:image" content="https://vue-bits.dev/og-pic.jpg" />
<meta property="og:image:height" content="630"> <meta name="twitter:image:alt" content="The Vue Bits landing page design, showcasing the logo and a subtitle!" />
<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 --> <!-- Favicon & Apple Touch Icons -->
<meta name="twitter:card" content="summary_large_image"> <link rel="icon" href="/favicon.ico" />
<meta name="twitter:title" content="Vue Bits"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="twitter:description" <link rel="manifest" href="/site.webmanifest" />
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 --> <!-- Canonical & Robots Meta Tags -->
<link rel="icon" href="/favicon.ico"> <link rel="canonical" href="https://vue-bits.dev" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <meta name="robots" content="index, follow" />
<link rel="manifest" href="/site.webmanifest">
<!-- Canonical & Robots Meta Tags --> <!-- Structured Data (JSON-LD for SEO) -->
<link rel="canonical" href="https://vue-bits.dev"> <script type="application/ld+json">
<meta name="robots" content="index, follow"> {
"@context": "https://schema.org",
<!-- Structured Data (JSON-LD for SEO) --> "@type": "WebPage",
<script type="application/ld+json"> "name": "Vue Bits",
{ "url": "https://vue-bits.dev",
"@context": "https://schema.org", "description": "An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces.",
"@type": "WebPage", "image": "https://vue-bits.dev/og-pic.jpg",
"name": "Vue Bits", "author": {
"url": "https://vue-bits.dev", "@type": "Person",
"description": "An open source collection of high quality, animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces.", "name": "David Haz"
"image": "https://vue-bits.dev/og-pic.jpg", },
"author": { "publisher": {
"@type": "Person", "@type": "Person",
"name": "David Haz" "name": "David Haz",
}, "logo": {
"publisher": { "@type": "ImageObject",
"@type": "Person", "url": "https://davidhaz.com"
"name": "David Haz", }
"logo": { }
"@type": "ImageObject",
"url": "https://davidhaz.com"
} }
} </script>
} <title>Vue Bits - Animated UI Components For Vue</title>
</script> </head>
<title>Vue Bits - Animated UI Components For Vue</title>
</head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html>
</html>

View File

@@ -1,37 +1,37 @@
{ {
"$schema": "https://unpkg.com/jsrepo@1.30.1/schemas/registry-config.json", "$schema": "https://unpkg.com/jsrepo@1.30.1/schemas/registry-config.json",
"meta": { "meta": {
"authors": ["David Haz"], "authors": ["David Haz"],
"description": "An open source collection of animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces.", "description": "An open source collection of animated, interactive & fully customizable Vue components for building stunning, memorable user interfaces.",
"bugs": "https://github.com/DavidHDev/vue-bits/issues", "bugs": "https://github.com/DavidHDev/vue-bits/issues",
"homepage": "https://vue-bits.dev", "homepage": "https://vue-bits.dev",
"repository": "https://github.com/DavidHDev/vue-bits", "repository": "https://github.com/DavidHDev/vue-bits",
"tags": [ "tags": [
"vue", "vue",
"javascript", "javascript",
"components", "components",
"web", "web",
"vuejs", "vuejs",
"css-animations", "css-animations",
"component-library", "component-library",
"ui-components", "ui-components",
"3d", "3d",
"ui-library", "ui-library",
"tailwind", "tailwind",
"tailwindcss", "tailwindcss",
"components", "components",
"components-library" "components-library"
] ]
}, },
"dirs": [], "dirs": [],
"doNotListBlocks": [], "doNotListBlocks": [],
"doNotListCategories": [], "doNotListCategories": [],
"listBlocks": [], "listBlocks": [],
"listCategories": [], "listCategories": [],
"excludeDeps": ["vue"], "excludeDeps": ["vue"],
"includeBlocks": [], "includeBlocks": [],
"includeCategories": [], "includeCategories": [],
"excludeBlocks": [], "excludeBlocks": [],
"excludeCategories": [], "excludeCategories": [],
"preview": true "preview": true
} }

1
package-lock.json generated
View File

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

View File

@@ -11,6 +11,8 @@
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"new:component": "node scripts/generateComponent.js" "new:component": "node scripts/generateComponent.js"
}, },
"dependencies": { "dependencies": {
@@ -47,6 +49,7 @@
"jsrepo": "^1.30.1", "jsrepo": "^1.30.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vite": "^7.0.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 fs from 'fs';
import path from "path"; import path from 'path';
import process from "process"; import process from 'process';
import { fileURLToPath } from "url"; import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length < 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); process.exit(1);
} }
@@ -17,12 +17,12 @@ const [componentType, componentName] = args;
const componentNameLower = componentName.charAt(0).toLowerCase() + componentName.slice(1); const componentNameLower = componentName.charAt(0).toLowerCase() + componentName.slice(1);
const paths = { const paths = {
content: path.join(__dirname, "../src/content", componentType, componentName), content: path.join(__dirname, '../src/content', componentType, componentName),
demo: path.join(__dirname, "../src/demo", componentType), demo: path.join(__dirname, '../src/demo', componentType),
constants: path.join(__dirname, "../src/constants/code", componentType), constants: path.join(__dirname, '../src/constants/code', componentType)
}; };
Object.values(paths).forEach((dir) => { Object.values(paths).forEach(dir => {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
} }
@@ -31,12 +31,12 @@ Object.values(paths).forEach((dir) => {
const files = [ const files = [
path.join(paths.content, `${componentName}.vue`), path.join(paths.content, `${componentName}.vue`),
path.join(paths.demo, `${componentName}Demo.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)) { if (!fs.existsSync(file)) {
fs.writeFileSync(file, ""); fs.writeFileSync(file, '');
} }
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,45 @@
<template> <template>
<svg width="141" height="30" viewBox="0 0 193 41" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" <path
fill="white" /> d="M66.4663 34.2676L56.3843 7.12372H60.5722L68.7929 30.2348L77.0912 7.12372H81.2015L71.1195 34.2676H66.4663Z"
fill="white"
/>
<path <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" 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 <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" 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 <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" 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 <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" 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 <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" 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 <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" 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 <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" 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" /> <circle cx="29.3282" cy="10.6089" r="3.34281" fill="white" />
</svg> </svg>
</template> </template>
@@ -30,5 +47,5 @@
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ defineOptions({
name: 'VueBitsLogo' name: 'VueBitsLogo'
}) });
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,24 @@
<template> <template>
<main class="app-container"> <main class="app-container">
<Header /> <Header />
<section class="category-wrapper"> <section class="category-wrapper">
<Sidebar /> <Sidebar />
<div class="category-page"> <div class="category-page">
<router-view /> <router-view />
</div> </div>
</section> </section>
<Toast position="bottom-right"
<Toast
position="bottom-right"
:closeButtonProps="{ style: { right: '0', margin: '0', outline: 'none', border: 'none' } }" :closeButtonProps="{ style: { right: '0', margin: '0', outline: 'none', border: 'none' } }"
:pt="{ :pt="{
message: { message: {
style: { style: {
borderRadius: '10px', borderRadius: '10px',
border: '1px solid #142216', border: '1px solid #142216',
backgroundColor: '#0b0b0b', backgroundColor: '#0b0b0b'
} }
}, },
messageContent: { messageContent: {
@@ -27,11 +31,12 @@
display: 'none' display: 'none'
} }
} }
}" /> }"
/>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Header from '../navs/Header.vue'; import Header from '../navs/Header.vue';
import Sidebar from '../navs/Sidebar.vue'; import Sidebar from '../navs/Sidebar.vue';
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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