mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-09 16:39:31 -06:00
Landing Page
This commit is contained in:
310
src/components/landing/DisplayHeader/DisplayHeader.css
Normal file
310
src/components/landing/DisplayHeader/DisplayHeader.css
Normal file
@@ -0,0 +1,310 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
justify-content: space-between;
|
||||
width: 100vw;
|
||||
padding: 0 4em;
|
||||
height: 160px;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(to bottom, #0e0e0e, transparent);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav-cta-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.logo::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
background: transparent;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
mask: radial-gradient(ellipse at center, black 0%, black 20%, transparent 80%);
|
||||
-webkit-mask: radial-gradient(ellipse at center, black 0%, black 20%, transparent 80%);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 26px;
|
||||
width: auto;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.landing-nav-items {
|
||||
display: none;
|
||||
color: #fff;
|
||||
height: 60px;
|
||||
padding: 0 2.4rem 0 calc(2.4rem + 6px);
|
||||
border-radius: 50px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 135, 62, 0.15);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-weight: 400;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.active-link {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.active-link::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
left: -12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
font-weight: 500;
|
||||
padding: 0 0 0 1.4rem;
|
||||
height: calc(60px - 2px);
|
||||
background: linear-gradient(135deg,
|
||||
rgb(30, 160, 63),
|
||||
rgba(24, 47, 255, 0.6));
|
||||
background-size: 200% 200%;
|
||||
backdrop-filter: blur(25px);
|
||||
-webkit-backdrop-filter: blur(25px);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
justify-content: space-between;
|
||||
transition: .3s ease;
|
||||
}
|
||||
|
||||
.cta-button span {
|
||||
background-color: #0e0e0e;
|
||||
margin-left: 1em;
|
||||
margin-right: calc(1em - 8px);
|
||||
padding-top: .1em;
|
||||
height: 45px;
|
||||
border-radius: 50px;
|
||||
width: 100px;
|
||||
font-weight: 600;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cta-button span img {
|
||||
margin-right: 6px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: .3s ease;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
transition: .3s ease;
|
||||
}
|
||||
|
||||
.cta-button:hover span img {
|
||||
transform: scale(1.2);
|
||||
transition: .3s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.header {
|
||||
padding: 0 2em;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.logo::before {
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
padding: 0 0 0 1rem;
|
||||
height: 50px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cta-button span {
|
||||
height: 38px;
|
||||
width: 80px;
|
||||
margin-left: 0.8em;
|
||||
margin-right: calc(0.8em - 6px);
|
||||
}
|
||||
|
||||
.cta-button span img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.header {
|
||||
padding: 0 1.5em;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.logo::before {
|
||||
width: 160px;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
padding: 0 0 0 0.8rem;
|
||||
height: 45px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.cta-button span {
|
||||
height: 35px;
|
||||
width: 60px !important;
|
||||
margin-left: 0.6em;
|
||||
margin-right: calc(0.6em - 4px);
|
||||
}
|
||||
|
||||
.cta-button span img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header {
|
||||
padding: 0 1rem;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.logo::before {
|
||||
width: 140px;
|
||||
height: 95px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
padding: 0 0 0 0.6rem;
|
||||
height: 40px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.cta-button span {
|
||||
height: 32px;
|
||||
width: 60px;
|
||||
margin-left: 0.5em;
|
||||
margin-right: calc(0.5em - 3px);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cta-button span img {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 375px) {
|
||||
.header {
|
||||
padding: 0 0.8rem;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.logo::before {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
padding: 0 0 0 0.5rem;
|
||||
height: 36px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.cta-button span {
|
||||
height: 28px;
|
||||
width: 50px;
|
||||
margin-left: 0.4em;
|
||||
margin-right: calc(0.4em - 2px);
|
||||
}
|
||||
|
||||
.cta-button span img {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.landing-nav-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav-cta-group {
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
77
src/components/landing/DisplayHeader/DisplayHeader.vue
Normal file
77
src/components/landing/DisplayHeader/DisplayHeader.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="header-container">
|
||||
<router-link to="/" class="logo">
|
||||
<VueBitsLogo />
|
||||
</router-link>
|
||||
|
||||
<div class="nav-cta-group">
|
||||
<nav class="landing-nav-items" ref="navRef">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:class="{ 'active-link': activeItem === 'home' }"
|
||||
to="/"
|
||||
>
|
||||
Home
|
||||
</router-link>
|
||||
<router-link class="nav-link" to="/text-animations/split-text">
|
||||
Docs
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
class="cta-button"
|
||||
@click="openGitHub"
|
||||
>
|
||||
Star On GitHub
|
||||
<span ref="starCountRef" :style="{ opacity: 0 }">
|
||||
<img :src="starIcon" alt="Star Icon" />
|
||||
{{ stars }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import VueBitsLogo from '@/components/common/Logo.vue'
|
||||
import { useStars } from '@/composables/useStars'
|
||||
import starIcon from '@/assets/common/star.svg'
|
||||
import './DisplayHeader.css'
|
||||
|
||||
interface Props {
|
||||
activeItem?: string | null;
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const navRef = ref<HTMLElement | null>(null)
|
||||
const starCountRef = ref<HTMLElement | null>(null)
|
||||
const stars = useStars()
|
||||
|
||||
const openGitHub = () => {
|
||||
window.open('https://github.com/DavidHDev/vue-bits', '_blank')
|
||||
}
|
||||
|
||||
watch(stars, (newStars) => {
|
||||
if (newStars && starCountRef.value) {
|
||||
gsap.fromTo(starCountRef.value,
|
||||
{
|
||||
scale: 0,
|
||||
width: 0,
|
||||
opacity: 0
|
||||
},
|
||||
{
|
||||
scale: 1,
|
||||
width: "100px",
|
||||
opacity: 1,
|
||||
duration: 0.8,
|
||||
ease: "back.out(1)"
|
||||
}
|
||||
)
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
662
src/components/landing/FeatureCards/FeatureCards.css
Normal file
662
src/components/landing/FeatureCards/FeatureCards.css
Normal file
@@ -0,0 +1,662 @@
|
||||
.features-section {
|
||||
position: relative;
|
||||
margin-top: 12em;
|
||||
padding: 8rem 2rem 4em;
|
||||
padding-bottom: 0 !important;
|
||||
z-index: 22;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.features-header {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 4rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -2px;
|
||||
color: #fff;
|
||||
margin-bottom: .2rem;
|
||||
background: linear-gradient(135deg, #fff 0%, #60fa89 20%, #55f788 40%, #00ff62 60%, #55f799 80%, #fff 100%);
|
||||
background-size: 200% 200%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-align: center;
|
||||
animation: gradientShift 4s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.features-title * {
|
||||
background: inherit;
|
||||
background-size: inherit;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: inherit;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.features-subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: #fff;
|
||||
text-shadow:
|
||||
0 0 2px rgba(255, 255, 255, 0.1),
|
||||
0 0 4px rgba(255, 255, 255, 0.3),
|
||||
0 0 8px rgba(255, 255, 255, 0.4),
|
||||
0 0 136px rgba(0, 255, 98, 0.9);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.features-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.features-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.features-header {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.features-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.features-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bento-grid {
|
||||
max-width: 1200px;
|
||||
display: grid;
|
||||
gap: 1.5em;
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) and (max-width: 767px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.25em;
|
||||
}
|
||||
|
||||
.card1 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
.card2 {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
.card4 {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@media (min-width: 50rem) {
|
||||
.bento-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(3, auto);
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.card1 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
.card2 {
|
||||
grid-column: 3 / 5;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.card4 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 49.99rem) {
|
||||
.bento-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.25em;
|
||||
}
|
||||
|
||||
.card1 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
.card2 {
|
||||
grid-column: 3 / 4;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.card4 {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
user-select: none;
|
||||
background: #0e0e0e;
|
||||
border: 1px solid rgba(148, 184, 154, 0.2);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
--glow-x: 50%;
|
||||
--glow-y: 50%;
|
||||
--glow-intensity: 0;
|
||||
}
|
||||
|
||||
.feature-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(163, 148, 184, 0.3), transparent);
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.feature-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 1px;
|
||||
background: radial-gradient(200px circle at var(--glow-x) var(--glow-y),
|
||||
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
|
||||
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
|
||||
transparent 60%);
|
||||
border-radius: inherit;
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: subtract;
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
box-shadow: 0 4px 40px -15px rgba(46, 24, 78, 0.4) !important;
|
||||
background: #07160a;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.01em;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.feature-card h2 {
|
||||
font-size: 6rem;
|
||||
position: relative;
|
||||
top: 22px;
|
||||
margin: 0;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #fff 0%, #60fa7f 20%, #55f783 40%, #00ff48 60%, #58f755 80%, #fff 100%);
|
||||
background-size: 200% 200%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: gradientShift 4s ease-in-out infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: rgba(161, 148, 184, 0.9);
|
||||
line-height: 1.4;
|
||||
font-size: 0.95rem;
|
||||
text-align: left;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.particle-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(0, 255, 98, 0.8);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 6px rgba(0, 255, 68, 0.6);
|
||||
}
|
||||
|
||||
.particle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: rgba(0, 255, 81, 0.3);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.feature-card.particle-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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);
|
||||
background: #07160b;
|
||||
}
|
||||
|
||||
.global-spotlight {
|
||||
mix-blend-mode: screen;
|
||||
will-change: transform, opacity;
|
||||
z-index: 200 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.components-gif-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .components-gif-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.components-gif {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
opacity: 0.2;
|
||||
mix-blend-mode: lighten;
|
||||
display: block;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.components-gif-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||
border-radius: 0 0 8px 8px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.components-gif-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.messages-gif-wrapper {
|
||||
position: absolute;
|
||||
mix-blend-mode: lighten;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
width: 50%;
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .messages-gif-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.messages-gif {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
opacity: 0.3;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.messages-gif-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||
border-radius: 0 0 8px 8px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.messages-gif-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.switch-gif-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1rem;
|
||||
width: 40%;
|
||||
mix-blend-mode: lighten;
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .switch-gif-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.switch-gif {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
opacity: 0.3;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.switch-gif-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||
border-radius: 0 0 8px 8px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.switch-gif-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.features-section {
|
||||
padding: 4rem 1rem 2rem;
|
||||
padding-bottom: 0;
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
.bento-grid {
|
||||
gap: 1rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
min-height: 160px;
|
||||
padding: 1.25rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-card h2 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.components-gif-wrapper,
|
||||
.messages-gif-wrapper,
|
||||
.switch-gif-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.features-section {
|
||||
padding: 2.5rem 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.bento-grid {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
min-height: 150px;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-card h2 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.components-gif-wrapper,
|
||||
.messages-gif-wrapper,
|
||||
.switch-gif-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.features-section {
|
||||
padding: 6rem 2rem 3rem;
|
||||
margin-top: 6em;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
min-height: 200px;
|
||||
padding: 1.75rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.components-gif-wrapper,
|
||||
.messages-gif-wrapper,
|
||||
.switch-gif-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 50rem) {
|
||||
.features-section {
|
||||
padding: 8rem 2rem 4rem;
|
||||
padding-bottom: 0;
|
||||
margin-top: 8em;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
min-height: 220px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.components-gif-wrapper,
|
||||
.messages-gif-wrapper,
|
||||
.switch-gif-wrapper {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 500px) and (orientation: landscape) {
|
||||
.features-section {
|
||||
margin-top: 2em;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
min-height: 140px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
286
src/components/landing/FeatureCards/FeatureCards.vue
Normal file
286
src/components/landing/FeatureCards/FeatureCards.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div class="features-section">
|
||||
<div class="features-container">
|
||||
<div class="features-header">
|
||||
<h3 class="features-title">Zero cost, all the cool.</h3>
|
||||
<p class="features-subtitle">Everything you need to add flair to your websites</p>
|
||||
</div>
|
||||
|
||||
<GlobalSpotlight v-if="gridRef" :grid-ref="gridRef" :disable-animations="isMobile" />
|
||||
|
||||
<div class="bento-grid" ref="gridRef">
|
||||
<ParticleCard class="feature-card card1" :disable-animations="isMobile">
|
||||
<div className="messages-gif-wrapper">
|
||||
<img src="/assets/messages.gif" alt="Messages animation" className="messages-gif" />
|
||||
</div>
|
||||
<h2>
|
||||
<template v-if="isMobile">100</template>
|
||||
<CountUp v-else :to="100" />%
|
||||
</h2>
|
||||
<h3>Free & Open Source</h3>
|
||||
<p>Loved by developers around the world</p>
|
||||
</ParticleCard>
|
||||
|
||||
<ParticleCard class="feature-card card2" :disable-animations="isMobile">
|
||||
<div className="components-gif-wrapper">
|
||||
<img src="/assets/components.gif" alt="Components animation" className="components-gif" />
|
||||
</div>
|
||||
<h2>
|
||||
<template v-if="isMobile">80</template>
|
||||
<CountUp v-else :to="80" />+
|
||||
</h2>
|
||||
<h3>Curated Components</h3>
|
||||
<p>Growing weekly & only getting better</p>
|
||||
</ParticleCard>
|
||||
|
||||
<ParticleCard class="feature-card card4" :disable-animations="isMobile">
|
||||
<h2>
|
||||
Modern
|
||||
</h2>
|
||||
<h3>Technologies</h3>
|
||||
<p>TypeScript + Tailwind, ready to ship</p>
|
||||
</ParticleCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, defineComponent, h } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import CountUp from '../../../content/Animations/CountUp/CountUp.vue'
|
||||
import './FeatureCards.css'
|
||||
|
||||
const isMobile = ref(false)
|
||||
const gridRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
const checkIsMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkIsMobile()
|
||||
window.addEventListener('resize', checkIsMobile)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkIsMobile)
|
||||
})
|
||||
|
||||
const ParticleCard = defineComponent({
|
||||
name: 'ParticleCard',
|
||||
props: {
|
||||
disableAnimations: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const cardRef = ref<HTMLDivElement | null>(null)
|
||||
const particlesRef = ref<HTMLDivElement[]>([])
|
||||
const timeoutsRef = ref<number[]>([])
|
||||
const isHoveredRef = ref(false)
|
||||
const memoizedParticles = ref<HTMLDivElement[]>([])
|
||||
const particlesInit = ref(false)
|
||||
|
||||
const createParticle = (x: number, y: number): HTMLDivElement => {
|
||||
const el = document.createElement('div')
|
||||
el.className = 'particle'
|
||||
el.style.cssText = `
|
||||
position:absolute;width:4px;height:4px;border-radius:50%;
|
||||
background:rgba(132,0,255,1);box-shadow:0 0 6px rgba(132,0,255,.6);
|
||||
pointer-events:none;z-index:100;left:${x}px;top:${y}px;
|
||||
`
|
||||
return el
|
||||
}
|
||||
|
||||
const memoizeParticles = () => {
|
||||
if (particlesInit.value || !cardRef.value) return
|
||||
const { width, height } = cardRef.value.getBoundingClientRect()
|
||||
Array.from({ length: 12 }).forEach(() => {
|
||||
memoizedParticles.value.push(createParticle(Math.random() * width, Math.random() * height))
|
||||
})
|
||||
particlesInit.value = true
|
||||
}
|
||||
|
||||
const clearParticles = () => {
|
||||
timeoutsRef.value.forEach(clearTimeout)
|
||||
timeoutsRef.value = []
|
||||
particlesRef.value.forEach(p =>
|
||||
gsap.to(p, {
|
||||
scale: 0,
|
||||
opacity: 0,
|
||||
duration: 0.3,
|
||||
ease: "back.in(1.7)",
|
||||
onComplete: () => {
|
||||
if (p.parentNode) {
|
||||
p.parentNode.removeChild(p)
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
particlesRef.value = []
|
||||
}
|
||||
|
||||
const animateParticles = () => {
|
||||
if (!cardRef.value || !isHoveredRef.value) return
|
||||
if (!particlesInit.value) memoizeParticles()
|
||||
|
||||
memoizedParticles.value.forEach((particle, i) => {
|
||||
const id = setTimeout(() => {
|
||||
if (!isHoveredRef.value || !cardRef.value) return
|
||||
const clone = particle.cloneNode(true) as HTMLDivElement
|
||||
cardRef.value.appendChild(clone)
|
||||
particlesRef.value.push(clone)
|
||||
|
||||
gsap.set(clone, { scale: 0, opacity: 0 })
|
||||
gsap.to(clone, { scale: 1, opacity: 1, duration: 0.3, ease: "back.out(1.7)" })
|
||||
gsap.to(clone, {
|
||||
x: (Math.random() - 0.5) * 100,
|
||||
y: (Math.random() - 0.5) * 100,
|
||||
rotation: Math.random() * 360,
|
||||
duration: 2 + Math.random() * 2,
|
||||
ease: "none",
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
})
|
||||
gsap.to(clone, { opacity: 0.3, duration: 1.5, ease: "power2.inOut", repeat: -1, yoyo: true })
|
||||
}, i * 100)
|
||||
timeoutsRef.value.push(id)
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
isHoveredRef.value = true
|
||||
animateParticles()
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHoveredRef.value = false
|
||||
clearParticles()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.disableAnimations || !cardRef.value) return
|
||||
|
||||
const node = cardRef.value
|
||||
node.addEventListener('mouseenter', handleMouseEnter)
|
||||
node.addEventListener('mouseleave', handleMouseLeave)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cardRef.value) {
|
||||
cardRef.value.removeEventListener('mouseenter', handleMouseEnter)
|
||||
cardRef.value.removeEventListener('mouseleave', handleMouseLeave)
|
||||
}
|
||||
isHoveredRef.value = false
|
||||
clearParticles()
|
||||
})
|
||||
|
||||
return () => h('div', {
|
||||
ref: cardRef,
|
||||
class: 'particle-container',
|
||||
style: { position: 'relative', overflow: 'hidden' }
|
||||
}, slots.default?.())
|
||||
}
|
||||
})
|
||||
|
||||
const GlobalSpotlight = defineComponent({
|
||||
name: 'GlobalSpotlight',
|
||||
props: {
|
||||
gridRef: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
disableAnimations: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const spotlightRef = ref<HTMLDivElement | null>(null)
|
||||
const isInsideSectionRef = ref(false)
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!spotlightRef.value || !props.gridRef.value) return
|
||||
const section = props.gridRef.value.closest('.features-section')
|
||||
const rect = section?.getBoundingClientRect()
|
||||
const inside =
|
||||
rect &&
|
||||
e.clientX >= rect.left && e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top && e.clientY <= rect.bottom
|
||||
|
||||
isInsideSectionRef.value = inside
|
||||
const cards = props.gridRef.value.querySelectorAll('.feature-card')
|
||||
|
||||
if (!inside) {
|
||||
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
|
||||
cards.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
|
||||
return
|
||||
}
|
||||
|
||||
let minDist = Infinity
|
||||
const prox = 100, fade = 150
|
||||
cards.forEach((card: HTMLElement) => {
|
||||
const r = card.getBoundingClientRect()
|
||||
const cx = r.left + r.width / 2
|
||||
const cy = r.top + r.height / 2
|
||||
const d = Math.hypot(e.clientX - cx, e.clientY - cy) - Math.max(r.width, r.height) / 2
|
||||
const ed = Math.max(0, d)
|
||||
minDist = Math.min(minDist, ed)
|
||||
|
||||
const rx = ((e.clientX - r.left) / r.width) * 100
|
||||
const ry = ((e.clientY - r.top) / r.height) * 100
|
||||
let glow = 0
|
||||
if (ed <= prox) glow = 1
|
||||
else if (ed <= fade) glow = (fade - ed) / (fade - prox)
|
||||
card.style.setProperty('--glow-x', `${rx}%`)
|
||||
card.style.setProperty('--glow-y', `${ry}%`)
|
||||
card.style.setProperty('--glow-intensity', String(glow))
|
||||
})
|
||||
|
||||
gsap.to(spotlightRef.value, { left: e.clientX, top: e.clientY, duration: 0.1, ease: "power2.out" })
|
||||
const target = minDist <= prox ? 0.8 : minDist <= fade ? ((fade - minDist) / (fade - prox)) * 0.8 : 0
|
||||
gsap.to(spotlightRef.value, { opacity: target, duration: target > 0 ? 0.2 : 0.5, ease: "power2.out" })
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isInsideSectionRef.value = false
|
||||
props.gridRef.value
|
||||
?.querySelectorAll('.feature-card')
|
||||
.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
|
||||
if (spotlightRef.value) {
|
||||
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.disableAnimations || !props.gridRef?.value) return
|
||||
|
||||
const spotlight = document.createElement('div')
|
||||
spotlight.className = 'global-spotlight'
|
||||
spotlight.style.cssText = `
|
||||
position:fixed;width:800px;height:800px;border-radius:50%;pointer-events:none;
|
||||
background:radial-gradient(circle,rgba(132,0,255,.15) 0%,rgba(132,0,255,.08) 15%,
|
||||
rgba(132,0,255,.04) 25%,rgba(132,0,255,.02) 40%,rgba(132,0,255,.01) 65%,transparent 70%);
|
||||
z-index:200;opacity:0;transform:translate(-50%,-50%);mix-blend-mode:screen;
|
||||
`
|
||||
document.body.appendChild(spotlight)
|
||||
spotlightRef.value = spotlight
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseleave', handleMouseLeave)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseleave', handleMouseLeave)
|
||||
if (spotlightRef.value?.parentNode) {
|
||||
spotlightRef.value.parentNode.removeChild(spotlightRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
return () => null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
155
src/components/landing/Footer/Footer.css
Normal file
155
src/components/landing/Footer/Footer.css
Normal file
@@ -0,0 +1,155 @@
|
||||
.landing-footer {
|
||||
position: relative;
|
||||
margin-top: 8rem;
|
||||
padding: 2.4rem;
|
||||
border-top: 1px solid rgba(149, 184, 148, 0.1);
|
||||
background: linear-gradient(to bottom, transparent, #0e0e0e);
|
||||
z-index: 220;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-logo:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.footer-description {
|
||||
font-size: 1rem;
|
||||
color: rgba(161, 148, 184, 0.9);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.footer-heart {
|
||||
color: #27FF64;
|
||||
font-size: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.footer-creator-link {
|
||||
color: #27FF64;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-creator-link:hover {
|
||||
color: #ffffff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(161, 148, 184, 0.7);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
position: relative;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.landing-footer {
|
||||
margin-top: 6rem;
|
||||
padding: 3rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.landing-footer {
|
||||
margin-top: 4rem;
|
||||
padding: 2rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
37
src/components/landing/Footer/Footer.vue
Normal file
37
src/components/landing/Footer/Footer.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<FadeContent :blur="true" :duration="600">
|
||||
<footer class="landing-footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-left">
|
||||
<img :src="vueBitsLogo" alt="Vue Bits" class="footer-logo" />
|
||||
<p class="footer-description">
|
||||
A library created with <i class="pi pi-heart-fill footer-heart"></i> by
|
||||
<a href="https://davidhaz.com/" target="_blank" class="footer-creator-link">this guy</a>
|
||||
</p>
|
||||
<p class="footer-copyright">© {{ currentYear }} Vue Bits</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" rel="noopener noreferrer" class="footer-link">
|
||||
GitHub
|
||||
</a>
|
||||
<router-link to="/text-animations/split-text" class="footer-link">
|
||||
Docs
|
||||
</router-link>
|
||||
<a href="https://www.jsrepo.com/" target="_blank" class="footer-link">
|
||||
CLI
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</FadeContent>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import vueBitsLogo from '../../../assets/logos/vue-bits-logo.svg'
|
||||
import FadeContent from '@/content/Animations/FadeContent/FadeContent.vue'
|
||||
import './Footer.css'
|
||||
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
</script>
|
||||
122
src/components/landing/Hero/Hero.vue
Normal file
122
src/components/landing/Hero/Hero.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="landing-content">
|
||||
<div class="hero-main-content">
|
||||
<h1 class="landing-title">
|
||||
<ResponsiveSplitText :is-mobile="isMobile" text="Animated Vue components" class-name="hero-split"
|
||||
split-type="chars" :delay="30" :duration="2" ease="elastic.out(0.5, 0.3)" />
|
||||
<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)" />
|
||||
</h1>
|
||||
|
||||
<ResponsiveSplitText :is-mobile="isMobile" class-name="landing-subtitle" split-type="words" :delay="10"
|
||||
:duration="1" text="Eighty-plus snippets, ready to be dropped into your Vue projects" />
|
||||
|
||||
<router-link to="/text-animations/split-text" class="landing-button">
|
||||
<span>Browse Components</span>
|
||||
<div class="button-arrow-circle">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 12L10 8L6 4" stroke="#4c1d95" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="!isMobile" class="hero-cards-container">
|
||||
<div class="hero-card hero-card-1" @click="openUrl('https://vuebits.dev/backgrounds/dot-grid')">
|
||||
<div class="w-full h-full relative hero-dot-grid">
|
||||
<DotGrid base-color="#ffffff" active-color="rgba(138, 43, 226, 0.9)" :dot-size="8" :gap="16"
|
||||
:proximity="50" />
|
||||
<div class="placeholder-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-cards-row">
|
||||
<div class="hero-card hero-card-2" @click="openUrl('https://vuebits.dev/backgrounds/letter-glitch')">
|
||||
<LetterGlitch class-name="hero-glitch" :glitch-colors="['#ffffff', '#999999', '#333333']" />
|
||||
<div class="placeholder-card"></div>
|
||||
</div>
|
||||
|
||||
<div class="hero-card hero-card-3" @click="openUrl('https://vuebits.dev/backgrounds/squares')">
|
||||
<Squares border-color="#fff" :speed="0.2" direction="diagonal" hover-fill-color="#fff" />
|
||||
<div class="placeholder-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, h, defineComponent } from 'vue'
|
||||
import DotGrid from '@/content/Backgrounds/DotGrid/DotGrid.vue'
|
||||
import SplitText from '@/content/TextAnimations/SplitText/SplitText.vue'
|
||||
import LetterGlitch from '@/content/Backgrounds/LetterGlitch/LetterGlitch.vue'
|
||||
import Squares from '@/content/Backgrounds/Squares/Squares.vue'
|
||||
|
||||
const ResponsiveSplitText = defineComponent({
|
||||
props: {
|
||||
isMobile: { type: Boolean, required: true },
|
||||
text: { type: String, required: true },
|
||||
className: { type: String, default: '' },
|
||||
splitType: { type: String as () => 'chars' | 'words' | 'lines' | 'words, chars', default: 'chars' },
|
||||
delay: { type: Number, default: 100 },
|
||||
duration: { type: Number, default: 0.6 },
|
||||
ease: { type: String, default: 'power3.out' },
|
||||
from: { type: Object, default: () => ({ opacity: 0, y: 40 }) },
|
||||
to: { type: Object, default: () => ({ opacity: 1, y: 0 }) },
|
||||
threshold: { type: Number, default: 0.1 },
|
||||
rootMargin: { type: String, default: '-100px' },
|
||||
textAlign: { type: String as () => 'left' | 'center' | 'right' | 'justify', default: 'center' },
|
||||
onLetterAnimationComplete: { type: Function, default: undefined }
|
||||
},
|
||||
render() {
|
||||
if (this.isMobile) {
|
||||
return h('span', { class: this.className }, this.text)
|
||||
} else {
|
||||
return h(SplitText, {
|
||||
text: this.text,
|
||||
className: this.className,
|
||||
splitType: this.splitType,
|
||||
delay: this.delay,
|
||||
duration: this.duration,
|
||||
ease: this.ease,
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
threshold: this.threshold,
|
||||
rootMargin: this.rootMargin,
|
||||
textAlign: this.textAlign,
|
||||
onLetterAnimationComplete: this.onLetterAnimationComplete as (() => void) | undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
const isMobile = ref(false)
|
||||
|
||||
const checkIsMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkIsMobile()
|
||||
window.addEventListener('resize', checkIsMobile)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.placeholder-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
335
src/components/landing/PlasmaWave/PlasmaWave.vue
Normal file
335
src/components/landing/PlasmaWave/PlasmaWave.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div v-if="!isMobile" ref="containerRef" :style="{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
overflow: 'hidden',
|
||||
width: '100vw',
|
||||
height: '100vh'
|
||||
}">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { Renderer, Camera, Transform, Program, Mesh, Geometry } from 'ogl'
|
||||
|
||||
interface Props {
|
||||
xOffset?: number
|
||||
yOffset?: number
|
||||
rotationDeg?: number
|
||||
focalLength?: number
|
||||
speed1?: number
|
||||
speed2?: number
|
||||
dir2?: number
|
||||
bend1?: number
|
||||
bend2?: number
|
||||
fadeInDuration?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
rotationDeg: 0,
|
||||
focalLength: 0.8,
|
||||
speed1: 0.1,
|
||||
speed2: 0.1,
|
||||
dir2: 1.0,
|
||||
bend1: 0.9,
|
||||
bend2: 0.6,
|
||||
fadeInDuration: 2000
|
||||
})
|
||||
|
||||
const vertex = /* glsl */ `
|
||||
attribute vec2 position;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = position * 0.5 + 0.5;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
const fragment = /* glsl */ `
|
||||
precision mediump float;
|
||||
uniform float iTime;
|
||||
uniform vec2 iResolution;
|
||||
uniform vec2 uOffset;
|
||||
uniform float uRotation;
|
||||
uniform float focalLength;
|
||||
uniform float speed1;
|
||||
uniform float speed2;
|
||||
uniform float dir2;
|
||||
uniform float bend1;
|
||||
uniform float bend2;
|
||||
uniform float bendAdj1;
|
||||
uniform float bendAdj2;
|
||||
uniform float uOpacity;
|
||||
|
||||
const float lt = 0.05;
|
||||
const float pi = 3.141592653589793;
|
||||
const float pi2 = pi * 2.0;
|
||||
const float pi_2 = pi * 0.5;
|
||||
#define MAX_STEPS 15
|
||||
#define A(v) mat2(cos(m.v + radians(vec4(0.0,-90.0,90.0,0.0))))
|
||||
|
||||
void mainImage(out vec4 C, in vec2 U) {
|
||||
float t = iTime * pi;
|
||||
float s = 1.0;
|
||||
float d = 0.0;
|
||||
vec2 R = iResolution;
|
||||
vec2 m = vec2(0.0);
|
||||
|
||||
vec3 o = vec3(0.0, 0.0, -7.0);
|
||||
vec3 u = normalize(vec3((U - 0.5 * R) / R.y, focalLength));
|
||||
vec3 k = vec3(0.0);
|
||||
vec3 p;
|
||||
|
||||
mat2 v = A(y), h = A(x);
|
||||
|
||||
float t1 = t * 0.7;
|
||||
float t2 = t * 0.9;
|
||||
float tSpeed1 = t * speed1;
|
||||
float tSpeed2 = t * speed2 * dir2;
|
||||
|
||||
for (int step = 0; step < MAX_STEPS; ++step) {
|
||||
p = o + u * d;
|
||||
p.yz *= v;
|
||||
p.xz *= h;
|
||||
p.x -= 15.0;
|
||||
|
||||
float px = p.x;
|
||||
float wob1 = bend1 + bendAdj1 + sin(t1 + px * 0.8) * 0.1;
|
||||
float wob2 = bend2 + bendAdj2 + cos(t2 + px * 1.1) * 0.1;
|
||||
|
||||
vec2 baseOffset = vec2(px, px + pi_2);
|
||||
vec2 sinOffset = sin(baseOffset + tSpeed1) * wob1;
|
||||
vec2 cosOffset = cos(baseOffset + tSpeed2) * wob2;
|
||||
|
||||
float wSin = length(p.yz - sinOffset) - lt;
|
||||
float wCos = length(p.yz - cosOffset) - lt;
|
||||
|
||||
k.x = max(px + lt, wSin);
|
||||
k.y = max(px + lt, wCos);
|
||||
|
||||
s = min(s, min(k.x, k.y));
|
||||
if (s < 0.001 || d > 400.0) break;
|
||||
d += s * 0.7;
|
||||
}
|
||||
|
||||
vec3 c = max(cos(d * pi2) - s * sqrt(d) - k, 0.0);
|
||||
|
||||
// Vue.js colors: #42B883 (green) and #35495e (dark gray)
|
||||
vec3 vueGreen = vec3(0.259, 0.722, 0.514); // #42B883
|
||||
vec3 vueDark = vec3(0.208, 0.286, 0.369); // #35495e
|
||||
|
||||
// Use different colors for different wave components
|
||||
vec3 finalColor = vec3(0.0);
|
||||
if (k.x < k.y) {
|
||||
// First wave component - Vue green
|
||||
finalColor = vueGreen * c.x;
|
||||
} else {
|
||||
// Second wave component - Vue dark gray
|
||||
finalColor = vueDark * c.y;
|
||||
}
|
||||
|
||||
float intensity = max(finalColor.r, max(finalColor.g, finalColor.b));
|
||||
if (intensity < 0.15) discard;
|
||||
C = vec4(finalColor * (0.4 + intensity * 0.6), uOpacity);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 coord = gl_FragCoord.xy + uOffset;
|
||||
coord -= 0.5 * iResolution;
|
||||
float c = cos(uRotation), s = sin(uRotation);
|
||||
coord = mat2(c, -s, s, c) * coord;
|
||||
coord += 0.5 * iResolution;
|
||||
|
||||
vec4 color;
|
||||
mainImage(color, coord);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`
|
||||
|
||||
const isMobile = ref(false)
|
||||
const isVisible = ref(true)
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const uniformOffset = ref(new Float32Array([props.xOffset, props.yOffset]))
|
||||
const uniformResolution = ref(new Float32Array([1, 1]))
|
||||
const rendererRef = ref<Renderer | null>(null)
|
||||
const fadeStartTime = ref<number | null>(null)
|
||||
const lastTimeRef = ref(0)
|
||||
const pausedTimeRef = ref(0)
|
||||
const rafId = ref<number | null>(null)
|
||||
const resizeObserver = ref<ResizeObserver | null>(null)
|
||||
const intersectionObserver = ref<IntersectionObserver | null>(null)
|
||||
|
||||
const checkIsMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
}
|
||||
|
||||
const resize = () => {
|
||||
if (!containerRef.value || !rendererRef.value) return
|
||||
|
||||
const { width, height } = containerRef.value.getBoundingClientRect()
|
||||
rendererRef.value.setSize(width, height)
|
||||
uniformResolution.value[0] = width * rendererRef.value.dpr
|
||||
uniformResolution.value[1] = height * rendererRef.value.dpr
|
||||
|
||||
const gl = rendererRef.value.gl
|
||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
gl.clear(gl.COLOR_BUFFER_BIT)
|
||||
}
|
||||
|
||||
const initWebGL = () => {
|
||||
if (isMobile.value || !containerRef.value) return
|
||||
|
||||
const renderer = new Renderer({
|
||||
alpha: true,
|
||||
dpr: Math.min(window.devicePixelRatio, 1),
|
||||
antialias: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
powerPreference: 'high-performance',
|
||||
})
|
||||
rendererRef.value = renderer
|
||||
|
||||
const gl = renderer.gl
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
containerRef.value.appendChild(gl.canvas)
|
||||
|
||||
const camera = new Camera(gl)
|
||||
const scene = new Transform()
|
||||
|
||||
const geometry = new Geometry(gl, {
|
||||
position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) },
|
||||
})
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex,
|
||||
fragment,
|
||||
uniforms: {
|
||||
iTime: { value: 0 },
|
||||
iResolution: { value: uniformResolution.value },
|
||||
uOffset: { value: uniformOffset.value },
|
||||
uRotation: { value: 0 },
|
||||
focalLength: { value: props.focalLength },
|
||||
speed1: { value: props.speed1 },
|
||||
speed2: { value: props.speed2 },
|
||||
dir2: { value: props.dir2 },
|
||||
bend1: { value: props.bend1 },
|
||||
bend2: { value: props.bend2 },
|
||||
bendAdj1: { value: 0 },
|
||||
bendAdj2: { value: 0 },
|
||||
uOpacity: { value: 0 },
|
||||
},
|
||||
})
|
||||
new Mesh(gl, { geometry, program }).setParent(scene)
|
||||
|
||||
resize()
|
||||
|
||||
resizeObserver.value = new ResizeObserver(resize)
|
||||
resizeObserver.value.observe(containerRef.value)
|
||||
|
||||
const loop = (now: number) => {
|
||||
if (isVisible.value) {
|
||||
if (lastTimeRef.value === 0) {
|
||||
lastTimeRef.value = now - pausedTimeRef.value
|
||||
}
|
||||
|
||||
const t = (now - lastTimeRef.value) * 0.001
|
||||
|
||||
if (fadeStartTime.value === null && t > 0.1) {
|
||||
fadeStartTime.value = now
|
||||
}
|
||||
|
||||
let opacity = 0
|
||||
if (fadeStartTime.value !== null) {
|
||||
const fadeElapsed = now - fadeStartTime.value
|
||||
opacity = Math.min(fadeElapsed / props.fadeInDuration, 1)
|
||||
opacity = 1 - Math.pow(1 - opacity, 3)
|
||||
}
|
||||
|
||||
uniformOffset.value[0] = props.xOffset
|
||||
uniformOffset.value[1] = props.yOffset
|
||||
|
||||
program.uniforms.iTime.value = t
|
||||
program.uniforms.uRotation.value = props.rotationDeg * Math.PI / 180
|
||||
program.uniforms.focalLength.value = props.focalLength
|
||||
program.uniforms.uOpacity.value = opacity
|
||||
|
||||
renderer.render({ scene, camera })
|
||||
} else {
|
||||
if (lastTimeRef.value !== 0) {
|
||||
pausedTimeRef.value = now - lastTimeRef.value
|
||||
lastTimeRef.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
rafId.value = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
rafId.value = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
const setupIntersectionObserver = () => {
|
||||
if (!containerRef.value || isMobile.value) return
|
||||
|
||||
intersectionObserver.value = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isVisible.value = entry.isIntersecting
|
||||
},
|
||||
{
|
||||
rootMargin: '50px',
|
||||
threshold: 0.1,
|
||||
}
|
||||
)
|
||||
|
||||
intersectionObserver.value.observe(containerRef.value)
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (rafId.value) {
|
||||
cancelAnimationFrame(rafId.value)
|
||||
rafId.value = null
|
||||
}
|
||||
|
||||
if (resizeObserver.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
resizeObserver.value = null
|
||||
}
|
||||
|
||||
if (intersectionObserver.value) {
|
||||
intersectionObserver.value.disconnect()
|
||||
intersectionObserver.value = null
|
||||
}
|
||||
|
||||
if (rendererRef.value) {
|
||||
rendererRef.value.gl.canvas.remove()
|
||||
rendererRef.value = null
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', checkIsMobile)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkIsMobile()
|
||||
window.addEventListener('resize', checkIsMobile)
|
||||
|
||||
if (!isMobile.value) {
|
||||
initWebGL()
|
||||
setupIntersectionObserver()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
watch(isMobile, (newIsMobile) => {
|
||||
if (newIsMobile) {
|
||||
cleanup()
|
||||
} else {
|
||||
initWebGL()
|
||||
setupIntersectionObserver()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
162
src/components/landing/StartBuilding/StartBuilding.css
Normal file
162
src/components/landing/StartBuilding/StartBuilding.css
Normal file
@@ -0,0 +1,162 @@
|
||||
.start-building-section {
|
||||
width: 100%;
|
||||
padding: 80px 0;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
z-index: 22;
|
||||
}
|
||||
|
||||
.start-building-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.start-building-card {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
user-select: none;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(135deg,
|
||||
#3aed6d,
|
||||
rgba(24, 255, 93, 0.6));
|
||||
background-size: 200% 200%;
|
||||
border-radius: 16px;
|
||||
padding: 4rem 3rem;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.start-building-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('/assets/grain.webp');
|
||||
background-size: 500px 500px;
|
||||
filter: invert(100%);
|
||||
mix-blend-mode: multiply;
|
||||
background-repeat: repeat;
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.start-building-card > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.start-building-title {
|
||||
color: #0e0e0e;
|
||||
font-size: 2.6rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.start-building-subtitle {
|
||||
color: #0e0e0e;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin: -1rem 0 0 0;
|
||||
opacity: 0.9;
|
||||
max-width: 600px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.start-building-button {
|
||||
background: transparent;
|
||||
color: #0e0e0e;
|
||||
border: 2px solid #0e0e0e;
|
||||
padding: .6rem 1.6rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.start-building-button:hover {
|
||||
background: #0e0e0e;
|
||||
color: #27FF64;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.start-building-container {
|
||||
max-width: 1000px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.start-building-card {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.start-building-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.start-building-container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.start-building-card {
|
||||
padding: 3rem 2rem;
|
||||
border-radius: 12px;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.start-building-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.start-building-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.start-building-button {
|
||||
padding: 0.875rem 1.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.start-building-section {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.start-building-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.start-building-card {
|
||||
padding: 2.5rem 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.start-building-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.start-building-subtitle {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
padding: 0 0.6rem;
|
||||
}
|
||||
|
||||
.start-building-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
18
src/components/landing/StartBuilding/StartBuilding.vue
Normal file
18
src/components/landing/StartBuilding/StartBuilding.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<section class="start-building-section">
|
||||
<div class="start-building-container">
|
||||
<div class="start-building-card">
|
||||
<h2 class="start-building-title">Start exploring Vue Bits</h2>
|
||||
<p class="start-building-subtitle">Animations, components, backgrounds - it's all here</p>
|
||||
|
||||
<router-link to="/text-animations/split-text" class="start-building-button">
|
||||
Browse Components
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import './StartBuilding.css'
|
||||
</script>
|
||||
Reference in New Issue
Block a user