Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"astro": "^5.12.9",
"cloudinary": "^2.8.0",
"framer-motion": "^12.23.24",
"lenis": "^1.3.15",
"limax": "^4.1.0",
"lodash.merge": "^4.6.2",
"matter-js": "^0.20.0",
Expand Down
170 changes: 170 additions & 0 deletions src/components/core/LenisInit.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
/**
* Lenis Smooth Scrolling Component
* Initializes Lenis for butter-smooth scrolling experience
*/
---

<!-- Lenis CSS -->
<style is:global>
/* Lenis base styles */
html.lenis, html.lenis body {
height: auto;
}

.lenis.lenis-smooth {
scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}

.lenis.lenis-stopped {
overflow: hidden;
}

.lenis.lenis-smooth iframe {
pointer-events: none;
}

/* Scroll-based effects */
body.is-scrolling {
/* Elements can react to scrolling state */
}

body.is-scrolling-fast {
/* Fast scroll effects */
}

/* Smooth scroll anchor offset for fixed header */
:target {
scroll-margin-top: 80px;
}

/* Scroll-linked animations */
@supports (animation-timeline: scroll()) {
/* Progress bar at top of page */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg,
var(--color-primary, #6366f1) 0%,
var(--color-secondary, #a855f7) 100%
);
transform-origin: left;
transform: scaleX(0);
animation: scroll-progress-anim linear;
animation-timeline: scroll();
z-index: 9999;
}

@keyframes scroll-progress-anim {
to {
transform: scaleX(1);
}
}
}

/* Scroll reveal animations */
.scroll-fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-fade-in.is-visible {
opacity: 1;
transform: translateY(0);
}

.scroll-slide-left {
opacity: 0;
transform: translateX(-50px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-slide-left.is-visible {
opacity: 1;
transform: translateX(0);
}

.scroll-slide-right {
opacity: 0;
transform: translateX(50px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-slide-right.is-visible {
opacity: 1;
transform: translateX(0);
}

.scroll-scale-in {
opacity: 0;
transform: scale(0.9);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-scale-in.is-visible {
opacity: 1;
transform: scale(1);
}

/* Parallax effect helper classes */
.parallax-slow {
will-change: transform;
}

.parallax-fast {
will-change: transform;
}

/* Stagger animation delays */
.stagger-1 { transition-delay: 0.1s; }
.stagger-2 { transition-delay: 0.2s; }
.stagger-3 { transition-delay: 0.3s; }
.stagger-4 { transition-delay: 0.4s; }
.stagger-5 { transition-delay: 0.5s; }
.stagger-6 { transition-delay: 0.6s; }
</style>

<!-- Lenis initialization script -->
<script>
import '~/scripts/lenis-init.ts';

// Intersection Observer for scroll reveal animations
const setupScrollReveal = () => {
const observerOptions = {
root: null,
rootMargin: '0px 0px -50px 0px',
threshold: 0.1,
};

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
}
});
}, observerOptions);

// Observe all scroll-animated elements
document.querySelectorAll('.scroll-fade-in, .scroll-slide-left, .scroll-slide-right, .scroll-scale-in').forEach((el) => {
observer.observe(el);
});
};

// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupScrollReveal);
} else {
setupScrollReveal();
}

// Re-initialize on Astro page navigation
document.addEventListener('astro:page-load', setupScrollReveal);
</script>
110 changes: 110 additions & 0 deletions src/components/ui/ScrollToTop.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
/**
* Scroll to Top Button
* A floating button that appears when user scrolls down and smoothly scrolls back to top
*/
---

<!-- Scroll Progress Bar -->
<div class="scroll-progress" aria-hidden="true"></div>

<!-- Scroll to Top Button -->
<button
id="scroll-to-top"
class="fixed bottom-8 right-8 z-50 w-12 h-12 rounded-full bg-primary dark:bg-primary-dark text-white shadow-lg opacity-0 invisible translate-y-4 transition-all duration-300 hover:scale-110 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 group"
aria-label="Scroll to top"
data-i18n-aria="common.scrollToTop"
>
<svg
class="w-6 h-6 mx-auto transform transition-transform group-hover:-translate-y-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 10l7-7m0 0l7 7m-7-7v18"
></path>
</svg>
</button>

<style>
#scroll-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}

#scroll-to-top {
background: linear-gradient(135deg, var(--color-primary, #6366f1) 0%, var(--color-secondary, #a855f7) 100%);
}

#scroll-to-top:hover {
background: linear-gradient(135deg, var(--color-primary-dark, #4f46e5) 0%, var(--color-secondary-dark, #9333ea) 100%);
}

/* Pulse animation when visible */
#scroll-to-top.visible {
animation: subtle-pulse 2s ease-in-out infinite;
}

@keyframes subtle-pulse {
0%, 100% {
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3), 0 4px 6px -4px rgba(99, 102, 241, 0.3);
}
50% {
box-shadow: 0 10px 25px -3px rgba(99, 102, 241, 0.5), 0 4px 10px -4px rgba(99, 102, 241, 0.5);
}
}
</style>

<script>
const initScrollToTop = () => {
const button = document.getElementById('scroll-to-top');
if (!button) return;

// Show/hide button based on scroll position
const toggleVisibility = () => {
const scrollY = window.scrollY || document.documentElement.scrollTop;
const threshold = 300;

if (scrollY > threshold) {
button.classList.add('visible');
} else {
button.classList.remove('visible');
}
};

// Listen to scroll events
window.addEventListener('scroll', toggleVisibility, { passive: true });

// Also listen to Lenis scroll events for smoother updates
window.addEventListener('lenis-scroll', toggleVisibility);

// Scroll to top on click
button.addEventListener('click', () => {
// Use Lenis if available, otherwise fallback
if ((window as any).lenis) {
(window as any).lenis.scrollTo(0, { duration: 1.5 });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});

// Initial check
toggleVisibility();
};

// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initScrollToTop);
} else {
initScrollToTop();
}

// Re-initialize on Astro page navigation
document.addEventListener('astro:page-load', initScrollToTop);
</script>
2 changes: 2 additions & 0 deletions src/i18n/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export const ui = {
'common.filter': '筛选',
'common.sort': '排序',
'common.all': '全部',
'common.scrollToTop': '返回顶部',

// 表单
'form.name': '姓名',
Expand Down Expand Up @@ -753,6 +754,7 @@ export const ui = {
'common.filter': 'Filter',
'common.sort': 'Sort',
'common.all': 'All',
'common.scrollToTop': 'Back to Top',

// Form
'form.name': 'Name',
Expand Down
2 changes: 2 additions & 0 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '~/assets/styles/base.css';
import MetaTags from '~/components/core/MetaTags.astro';
import BasicScripts from '~/components/core/BasicScripts.astro';
import AOSInit from '~/components/core/AOSInit.astro';
import LenisInit from '~/components/core/LenisInit.astro';
import { defaultLang } from '~/i18n/languages';

const { meta = {} } = Astro.props;
Expand All @@ -19,6 +20,7 @@ const { meta = {} } = Astro.props;
<slot />
<BasicScripts />
<AOSInit />
<LenisInit />

<!-- Language initialization script -->
<script is:inline>
Expand Down
2 changes: 2 additions & 0 deletions src/layouts/PageLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Footer from '~/components/widgets/Footer.astro';
import Announcement from '~/components/widgets/Announcement.astro';
import PhysicsBackground from '~/components/widgets/PhysicsBackground.astro';
import ParticlesBackground from '~/components/widgets/ParticlesBackground.astro';
import ScrollToTop from '~/components/ui/ScrollToTop.astro';

const { meta } = Astro.props;
---
Expand All @@ -18,4 +19,5 @@ const { meta } = Astro.props;
<slot />
</main>
<Footer />
<ScrollToTop />
</Layout>
Loading
Loading