feat(nav): smooth page transitions, icon-free navigation, cross-page ultra-fast persistence, and optimized glassmorphism

This commit is contained in:
ssbg04 committed 2026-09-13 17:16:56 +08:00
1 parent 164e3a6bbc
commit 29017e84ba
18 files changed
+410 -177

No files matched your search

+2 -2
View File
@@ -68,5 +68,5 @@ Project Dedicated page
- fix the gallery page modal view the description is not readable
- in the certificates if the picture is portrait and it has many information the picture is not visible, make the modal scrollable and responsive -->
- fix that on mobile view in all of the pages in navigation it has huge space at top, keep the home page layout
- fix that in landscape or desktop view that the active color indicator jumps to home to where you click a tab, and fix that the navigation bar zooming out in landscape view
<!-- - fix that on mobile view in all of the pages in navigation it has huge space at top, keep the home page layout
- fix that in landscape or desktop view that the active color indicator jumps to home to where you click a tab, and fix that the navigation bar zooming out in landscape view -->
+2 -2
View File
@@ -107,11 +107,11 @@ export default function About({
};
return (
<section id="about" className="py-12 sm:py-16 relative">
<section id="about" className="pt-3 sm:pt-12 pb-12 sm:pb-16 relative">
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
{/* Section Header */}
<ScrollReveal variant="fade-up" className="mb-10">
<ScrollReveal variant="fade-up" className="mb-5 sm:mb-10">
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-border-custom">
<div>
<h2 className="text-3xl sm:text-4xl font-bold font-heading text-foreground-custom tracking-tight">
+5
View File
@@ -283,6 +283,8 @@ export default function Background() {
};
checkTier();
window.addEventListener('tier-change', checkTier);
document.addEventListener('astro:after-swap', checkTier);
document.addEventListener('astro:page-load', checkTier);
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setReducedMotion(true);
@@ -290,6 +292,8 @@ export default function Background() {
return () => {
window.removeEventListener('tier-change', checkTier);
document.removeEventListener('astro:after-swap', checkTier);
document.removeEventListener('astro:page-load', checkTier);
};
}, []);
@@ -809,6 +813,7 @@ export default function Background() {
animationFrameId = requestAnimationFrame(renderLoop);
if (!isVisible) return;
if (document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true') return;
const tSec = now * 0.001;
+3 -3
View File
@@ -316,11 +316,11 @@ export default function Certifications({ certifications, extraCerts = [], sectio
const hasBothTypes = documentCerts.length > 0 && skillBadges.length > 0;
return (
<section id="certifications" className="pt-6 sm:pt-10 pb-16 sm:pb-20 relative">
<section id="certifications" className="pt-3 sm:pt-10 pb-16 sm:pb-20 relative">
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
{/* Section Header */}
<ScrollReveal variant="fade-up" className="mb-8">
<ScrollReveal variant="fade-up" className="mb-4 sm:mb-8">
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-border-custom">
<div>
<h2 className="text-3xl sm:text-4xl font-bold font-heading text-foreground-custom tracking-tight">
@@ -332,7 +332,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
{/* ─── Category Filter Tabs (Shown when both document certificates and skill badges exist) ─── */}
{hasBothTypes && (
<ScrollReveal variant="fade-up" delay={50} className="mb-8">
<ScrollReveal variant="fade-up" delay={50} className="mb-5 sm:mb-8">
<div className="flex flex-wrap items-center gap-2 p-1.5 rounded-2xl bg-foreground-custom/[0.03] border border-border-custom w-fit">
<button
onClick={() => {
+5 -5
View File
@@ -175,18 +175,18 @@ export default function ContactForm({
};
return (
<section id="contact" className="py-24 relative overflow-hidden">
<div className="container mx-auto px-6 max-w-6xl relative z-10">
<section id="contact" className="pt-2 sm:pt-12 md:pt-16 pb-16 sm:pb-24 relative overflow-hidden">
<div className="container mx-auto px-4 sm:px-6 max-w-6xl relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 lg:gap-8 items-start">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-8 items-start">
{/* Left Side: Info */}
<div className="lg:col-span-2 flex flex-col justify-center h-full">
<ScrollReveal variant="fade-right">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black font-heading mb-5 leading-tight tracking-tight text-foreground-custom block">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black font-heading mb-3 sm:mb-5 leading-tight tracking-tight text-foreground-custom block">
{contactHeading}
</h2>
<p className="text-muted-foreground-custom text-base md:text-lg mb-8 leading-relaxed">
<p className="text-muted-foreground-custom text-base md:text-lg mb-5 sm:mb-8 leading-relaxed">
{contactSubtitle}
</p>
+35 -25
View File
@@ -23,28 +23,32 @@ export default function ControlsDropdown() {
setIsTouchDevice(window.matchMedia('(hover: none) and (pointer: coarse)').matches);
// Detect theme preferences: System default or manual stored
// Detect theme preferences: Dark default or manual stored
const stored = localStorage.getItem('theme') as ThemeMode | null;
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'light' || stored === 'dark') {
setThemeMode(stored);
setResolvedTheme(stored);
if (stored === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} else {
// Default: auto-detect system theme!
if (stored === 'light') {
setThemeMode('light');
setResolvedTheme('light');
document.documentElement.classList.remove('dark');
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#fafafa');
} else if (stored === 'system') {
setThemeMode('system');
const detectedTheme = systemDark ? 'dark' : 'light';
setResolvedTheme(detectedTheme);
if (systemDark) {
if (detectedTheme === 'dark') {
document.documentElement.classList.add('dark');
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#090a0f');
} else {
document.documentElement.classList.remove('dark');
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#fafafa');
}
} else {
// Default: dark mode signature theme
setThemeMode('dark');
setResolvedTheme('dark');
document.documentElement.classList.add('dark');
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#090a0f');
}
const checkLite = () => {
@@ -52,6 +56,8 @@ export default function ControlsDropdown() {
};
checkLite();
window.addEventListener('tier-change', checkLite);
document.addEventListener('astro:after-swap', checkLite);
document.addEventListener('astro:page-load', checkLite);
const savedMag = localStorage.getItem('magnifierEnabled');
setMagnifierEnabled(savedMag === 'true');
@@ -93,6 +99,8 @@ export default function ControlsDropdown() {
return () => {
window.removeEventListener('tier-change', checkLite);
document.removeEventListener('astro:after-swap', checkLite);
document.removeEventListener('astro:page-load', checkLite);
if (darkQuery.removeEventListener) {
darkQuery.removeEventListener('change', handleSystemThemeChange);
}
@@ -126,24 +134,26 @@ export default function ControlsDropdown() {
haptic.tap();
setThemeMode(mode);
let resolved: 'light' | 'dark' = 'dark';
if (mode === 'system') {
localStorage.removeItem('theme');
localStorage.setItem('theme', 'system');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setResolvedTheme(systemDark ? 'dark' : 'light');
if (systemDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
resolved = systemDark ? 'dark' : 'light';
} else {
localStorage.setItem('theme', mode);
setResolvedTheme(mode);
if (mode === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
resolved = mode;
}
setResolvedTheme(resolved);
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (resolved === 'dark') {
document.documentElement.classList.add('dark');
metaThemeColor?.setAttribute('content', '#090a0f');
} else {
document.documentElement.classList.remove('dark');
metaThemeColor?.setAttribute('content', '#fafafa');
}
window.dispatchEvent(new CustomEvent('theme-change', { detail: { theme: resolved } }));
};
const toggleMagnifier = () => {
+96 -45
View File
@@ -4,28 +4,55 @@ import haptic from '../lib/haptics';
const ControlsDropdown = lazy(() => import('./ControlsDropdown'));
const navItems = [
{ label: 'Home', href: '/', icon: 'fa-solid fa-house' },
{ label: 'Projects', href: '/projects', icon: 'fa-solid fa-diagram-project' },
{ label: 'About', href: '/about', icon: 'fa-solid fa-user' },
{ label: 'Certifications', href: '/certifications', icon: 'fa-solid fa-award' },
{ label: 'Gallery', href: '/gallery', icon: 'fa-solid fa-images' },
{ label: 'Contact', href: '/contact', icon: 'fa-solid fa-envelope' },
{ label: 'Home', href: '/' },
{ label: 'Projects', href: '/projects' },
{ label: 'About', href: '/about' },
{ label: 'Certifications', href: '/certifications' },
{ label: 'Gallery', href: '/gallery' },
{ label: 'Contact', href: '/contact' },
];
interface NavbarProps {
fullName?: string;
logoImage?: string;
currentPath?: string;
}
export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.png' }: NavbarProps) {
const [activePath, setActivePath] = useState('/');
export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.png', currentPath }: NavbarProps) {
const [activePath, setActivePath] = useState<string>(() => {
if (currentPath) return currentPath;
if (typeof window !== 'undefined') return window.location.pathname;
return '/';
});
const [drawerOpen, setDrawerOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined') {
if (currentPath) {
setActivePath(currentPath);
} else if (typeof window !== 'undefined') {
setActivePath(window.location.pathname);
}
}, [currentPath]);
// Sync route and close drawer on Astro page transitions & browser navigation
useEffect(() => {
const handleRoute = () => {
if (typeof window !== 'undefined') {
setActivePath(window.location.pathname);
}
setDrawerOpen(false);
};
document.addEventListener('astro:page-load', handleRoute);
document.addEventListener('astro:after-swap', handleRoute);
window.addEventListener('popstate', handleRoute);
return () => {
document.removeEventListener('astro:page-load', handleRoute);
document.removeEventListener('astro:after-swap', handleRoute);
window.removeEventListener('popstate', handleRoute);
};
}, []);
useEffect(() => {
@@ -34,7 +61,7 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
return () => window.removeEventListener('scroll', onScroll);
}, []);
// Close drawer on route change / resize
// Close drawer on resize
useEffect(() => {
const onResize = () => { if (window.innerWidth >= 768) setDrawerOpen(false); };
window.addEventListener('resize', onResize, { passive: true });
@@ -54,20 +81,26 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
}, [drawerOpen]);
const isActive = (href: string) => {
if (href === '/') return activePath === '/';
return activePath === href || activePath.startsWith(href + '/');
const current = (activePath || '').replace(/\/+$/, '') || '/';
const target = (href || '').replace(/\/+$/, '') || '/';
if (target === '/') return current === '/';
return current === target || current.startsWith(target + '/');
};
return (
<>
{/* ── Desktop / Landscape Fixed Left Sidebar (Permanent, no hamburger, no minimize/maximize) ── */}
<aside
className="hidden md:flex fixed top-0 left-0 bottom-0 z-40 w-64 h-screen flex-col justify-between border-r border-border-custom bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md p-5 select-none"
className="hidden md:flex fixed top-0 left-0 bottom-0 z-40 w-64 h-full flex-col border-r border-border-custom bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md select-none overflow-hidden overscroll-contain"
aria-label="Desktop sidebar navigation"
>
{/* Top: Brand Header - Only name, no icon and no full-stack developer label */}
<div className="flex flex-col gap-4">
<a href="/" className="flex items-center group focus:outline-none py-1">
<div className="shrink-0 px-5 pt-5 pb-3 flex flex-col gap-3">
<a
href="/"
onClick={() => setActivePath('/')}
className="flex items-center group focus:outline-none py-0.5"
>
<span className="font-heading font-bold text-base tracking-tight text-foreground-custom group-hover:text-primary-custom transition-colors truncate">
{fullName}
</span>
@@ -76,34 +109,43 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
<div className="h-px w-full bg-border-custom/80" />
</div>
{/* Center: Vertical Navigation Links */}
<nav className="flex flex-col gap-1 py-4 overflow-y-auto flex-1 my-2" aria-label="Sidebar main links">
{/* Center: Vertical Navigation Links (Text-only, no icons, min-h-0 prevents flexbox twitch loop) */}
<nav
className="flex-1 min-h-0 overflow-y-auto px-4 py-2 flex flex-col gap-1"
aria-label="Sidebar main links"
>
{navItems.map((item) => {
const active = isActive(item.href);
return (
<a
key={item.label}
href={item.href}
className={`flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-xs tracking-wide transition-all duration-200 ${
onClick={() => setActivePath(item.href)}
className={`group flex items-center justify-between px-3.5 py-2.5 rounded-xl text-sm transition-all duration-200 shrink-0 ${
active
? 'text-foreground-custom font-bold'
? 'text-foreground-custom font-semibold bg-foreground-custom/[0.04]'
: 'text-muted-foreground-custom hover:text-foreground-custom hover:bg-foreground-custom/5 font-medium'
}`}
>
<i className={`${item.icon} text-sm w-4 text-center shrink-0 transition-colors ${active ? 'text-primary-custom' : 'text-muted-foreground-custom/70'}`} />
<span className="truncate">{item.label}</span>
{active && (
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-primary-custom shrink-0" />
)}
<div className="flex items-center gap-2.5">
<span
className={`w-1.5 h-1.5 rounded-full transition-all duration-200 ${
active
? 'bg-primary-custom scale-100 shadow-[0_0_8px_var(--primary-color)]'
: 'bg-transparent scale-0'
}`}
/>
<span className="truncate">{item.label}</span>
</div>
</a>
);
})}
</nav>
{/* Bottom: Controls & Status */}
<div className="flex flex-col gap-3 pt-4 border-t border-border-custom/80">
<div className="shrink-0 px-5 py-4 border-t border-border-custom/80 flex flex-col gap-3 mt-auto bg-background-custom/40 dark:bg-[#090a0f]/40">
<div className="flex items-center justify-between">
<span className="text-[11px] font-mono text-muted-foreground-custom">
<span className="text-xs font-mono text-muted-foreground-custom">
Preferences
</span>
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
@@ -113,18 +155,18 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
<a
href="/contact"
className="flex items-center justify-center gap-2 w-full px-3.5 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-sm"
onClick={() => setActivePath('/contact')}
className="flex items-center justify-center w-full px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-sm"
>
<span>Get in Touch</span>
<i className="fa-solid fa-arrow-right text-[10px]" />
</a>
</div>
</aside>
{/* ── Mobile Sticky Top Header (Only on screens < md) ── */}
<header
className={`md:hidden nav-enter fixed top-0 inset-x-0 z-50 glass-nav transition-all duration-300 ${scrolled || drawerOpen ? 'shadow-md bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md' : ''}`}
style={{ height: '60px' }}
className={`md:hidden fixed top-0 inset-x-0 z-50 glass-nav transition-all duration-300 touch-manipulation ${scrolled || drawerOpen ? 'shadow-md bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md' : ''}`}
style={{ height: '60px', touchAction: 'manipulation' }}
>
<div className="max-w-7xl mx-auto px-4 h-full flex items-center justify-between gap-3">
{/* Brand - Only Name, no icon */}
@@ -168,10 +210,10 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
{/* ── Mobile Navigation Full-Screen Overlay (From under top navbar to bottom) ── */}
{drawerOpen && (
<div
className="md:hidden fixed inset-x-0 top-[60px] bottom-0 z-40 bg-background-custom/98 dark:bg-[#090a0f]/98 backdrop-blur-xl border-t border-border-custom flex flex-col justify-between overflow-hidden animate-modal-fade select-none"
className="md:hidden fixed inset-x-0 top-[60px] bottom-0 z-40 bg-background-custom dark:bg-[#090a0f] border-t border-border-custom flex flex-col justify-between overflow-hidden animate-modal-fade select-none"
style={{ height: 'calc(100dvh - 60px)' }}
>
{/* Scrollable Navigation Links */}
{/* Scrollable Navigation Links (Text-only, no icons, subtle active indicator dot) */}
<nav className="flex-1 overflow-y-auto px-5 py-6 flex flex-col gap-1.5" aria-label="Mobile navigation links">
{navItems.map((item) => {
const active = isActive(item.href);
@@ -179,18 +221,26 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
<a
key={item.label}
href={item.href}
onClick={() => setDrawerOpen(false)}
className={`flex items-center justify-between px-4 py-3.5 rounded-2xl text-base transition-all duration-200 ${
onClick={() => {
setActivePath(item.href);
setDrawerOpen(false);
}}
className={`group flex items-center justify-between px-4 py-3 rounded-xl text-base transition-all duration-200 ${
active
? 'text-primary-custom font-bold bg-primary-custom/10 border border-primary-custom/25'
? 'text-foreground-custom font-bold bg-foreground-custom/[0.04]'
: 'text-muted-foreground-custom hover:text-foreground-custom hover:bg-foreground-custom/5 font-medium'
}`}
>
<div className="flex items-center gap-3.5">
<i className={`${item.icon} text-base w-5 text-center transition-colors ${active ? 'text-primary-custom' : 'text-muted-foreground-custom/70'}`} />
<div className="flex items-center gap-3">
<span
className={`w-2 h-2 rounded-full transition-all duration-200 ${
active
? 'bg-primary-custom scale-100 shadow-[0_0_8px_var(--primary-color)]'
: 'bg-transparent scale-0'
}`}
/>
<span>{item.label}</span>
</div>
{active && <span className="w-2 h-2 rounded-full bg-primary-custom shrink-0 shadow-xs" />}
</a>
);
})}
@@ -200,23 +250,24 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
<div className="p-5 border-t border-border-custom bg-foreground-custom/[0.02] flex flex-col gap-3.5 shrink-0">
{/* Preferences / Settings row */}
<div className="flex items-center justify-between px-1 py-1">
<span className="text-xs font-mono text-muted-foreground-custom flex items-center gap-2">
<i className="fa-solid fa-sliders text-primary-custom" />
<span>Preferences &amp; Settings</span>
<span className="text-xs font-mono text-muted-foreground-custom">
Preferences &amp; Settings
</span>
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<ControlsDropdown />
</Suspense>
</div>
{/* Mobile Contact Button */}
{/* Mobile Contact Button (Clean text, no icon) */}
<a
href="/contact"
onClick={() => setDrawerOpen(false)}
className="flex items-center justify-center gap-2 w-full px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-md"
onClick={() => {
setActivePath('/contact');
setDrawerOpen(false);
}}
className="flex items-center justify-center w-full px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-md"
>
<span>Get in Touch</span>
<i className="fa-solid fa-arrow-right text-[10px]" />
</a>
</div>
</div>
+1 -1
View File
@@ -156,7 +156,7 @@ export default function PhotoGallery({ items }: PhotoGalleryProps) {
return (
<div className="w-full">
{/* ─── Filter Bar ─── */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8 pb-4 border-b border-border-custom">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 mb-4 sm:mb-8 pb-3 sm:pb-4 border-b border-border-custom">
<div>
<h2 className="text-2xl sm:text-3xl font-bold font-heading text-foreground-custom tracking-tight">
Photo Gallery
+3 -3
View File
@@ -250,7 +250,7 @@ export default function PortfolioAssistantFAB() {
return (
<>
{/* ─── Floating Action Button (FAB) ─── */}
<div className="fixed bottom-6 right-5 sm:right-7 z-40">
<div className="fixed bottom-4 sm:bottom-6 right-4 sm:right-7 z-40">
<button
onClick={() => {
haptic.tap();
@@ -258,7 +258,7 @@ export default function PortfolioAssistantFAB() {
}}
aria-label={isOpen ? "Close Portfolio Assistant" : "Open Portfolio Assistant"}
aria-expanded={isOpen}
className="relative group p-3.5 rounded-2xl bg-primary-custom text-white shadow-xl hover:bg-primary-custom/90 hover:scale-105 active:scale-95 transition-all flex items-center gap-2 cursor-pointer border border-white/20"
className="relative group p-3 sm:p-3.5 rounded-2xl bg-primary-custom text-white shadow-xl hover:bg-primary-custom/90 hover:scale-105 active:scale-95 transition-all flex items-center gap-2 cursor-pointer border border-white/20"
>
{/* Animated ping circle */}
{!isOpen && (
@@ -274,7 +274,7 @@ export default function PortfolioAssistantFAB() {
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a.75.75 0 0 1-1.154-.63 4.887 4.887 0 0 0 1.28-3.138A7.834 7.834 0 0 1 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
</svg>
<span className="hidden sm:inline text-xs font-bold font-heading pr-0.5">
<span className="hidden md:inline landscape:hidden lg:landscape:inline text-xs font-bold font-heading pr-0.5">
Portfolio Assistant
</span>
</>
+107 -42
View File
@@ -1,5 +1,6 @@
---
import '../styles/global.css';
import { ClientRouter } from 'astro:transitions';
import BackToTop from '../components/BackToTop';
import Background from '../components/Background';
import SpotlightLens from '../components/SpotlightLens';
@@ -27,10 +28,10 @@ const isRootPage = Astro.url.pathname === '/';
---
<!doctype html>
<html lang="en">
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<link rel="icon" type="image/png" href="/logo.png" />
<link rel="apple-touch-icon" href="/logo.png" />
<!-- PWA & Mobile Web App Manifest -->
@@ -47,40 +48,56 @@ const isRootPage = Astro.url.pathname === '/';
<!-- FontAwesome 6 Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Theme Initialization Script (Prevents FOUC & auto-detects system theme) -->
<!-- Astro ClientRouter for seamless SPA page transitions (no reload, no twitch) -->
<ClientRouter />
<!-- Theme Initialization Script (Prevents FOUC & preserves theme on Astro transitions) -->
<script is:inline>
(function() {
function getResolvedTheme() {
var storedTheme = localStorage.getItem('theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (storedTheme === 'dark') return 'dark';
if (storedTheme === 'light') return 'light';
// Default: auto-detect system theme (storedTheme is null or 'system')
return prefersDark ? 'dark' : 'light';
if (storedTheme === 'dark') return 'dark';
if (storedTheme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Default: dark mode signature theme
return 'dark';
}
function applyTheme() {
function applyTheme(doc) {
var targetDoc = doc || document;
var theme = getResolvedTheme();
var metaThemeColor = targetDoc.querySelector('meta[name="theme-color"]');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
targetDoc.documentElement.classList.add('dark');
if (metaThemeColor) metaThemeColor.setAttribute('content', '#090a0f');
} else {
document.documentElement.classList.remove('dark');
targetDoc.documentElement.classList.remove('dark');
if (metaThemeColor) metaThemeColor.setAttribute('content', '#fafafa');
}
}
applyTheme();
applyTheme(document);
// Automatically adapt if user has not set a manual override or selected system
// Crucial for Astro ClientRouter: re-apply theme on before-swap, after-swap and page-load
document.addEventListener('astro:before-swap', function(e) {
if (e.newDocument) applyTheme(e.newDocument);
});
document.addEventListener('astro:after-swap', function() {
applyTheme(document);
});
document.addEventListener('astro:page-load', function() {
applyTheme(document);
});
// Automatically adapt if user explicitly selected system
try {
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
var listener = function(e) {
var stored = localStorage.getItem('theme');
if (!stored || stored === 'system') {
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
if (stored === 'system') {
applyTheme(document);
window.dispatchEvent(new CustomEvent('theme-change', { detail: { theme: e.matches ? 'dark' : 'light' } }));
}
};
@@ -98,37 +115,85 @@ const isRootPage = Astro.url.pathname === '/';
<!-- Orientation Stamp Script (Prevents nav layout shift) -->
<script is:inline>
(function() {
function setOrientation() {
function setOrientation(doc) {
var targetDoc = doc || document;
const isPortrait = window.matchMedia('(orientation: portrait)').matches;
const isNarrowLandscape = !isPortrait && window.innerWidth < 1024;
document.documentElement.dataset.orientation = isPortrait
targetDoc.documentElement.dataset.orientation = isPortrait
? 'portrait'
: isNarrowLandscape
? 'landscape-compact'
: 'landscape';
}
setOrientation();
window.addEventListener('resize', setOrientation, { passive: true });
window.addEventListener('orientationchange', setOrientation, { passive: true });
setOrientation(document);
window.addEventListener('resize', function() { setOrientation(document); }, { passive: true });
window.addEventListener('orientationchange', function() { setOrientation(document); }, { passive: true });
document.addEventListener('astro:before-swap', function(e) {
if (e.newDocument) setOrientation(e.newDocument);
});
document.addEventListener('astro:after-swap', function() {
setOrientation(document);
});
})();
</script>
<!-- Performance Tier & Lite Mode Detection -->
<!-- Performance Tier, Ultra-Fast Design & Lite Mode Detection (Persists across all page transitions) -->
<script is:inline>
(function() {
var manualLite = localStorage.getItem('liteMode');
var tier = 'mid';
if (manualLite === 'true') {
tier = 'low';
} else {
// Lite mode default is disabled (Off) unless explicitly enabled by user
var cores = navigator.hardwareConcurrency || 4;
if (cores >= 8 && window.devicePixelRatio >= 2) {
tier = 'high';
function applyTier(doc) {
var targetDoc = doc || document;
var manualLite = localStorage.getItem('liteMode');
var tier = 'mid';
if (manualLite === 'true') {
tier = 'low';
} else {
tier = 'mid';
// Lite mode default is disabled (Off) unless explicitly enabled by user
var cores = navigator.hardwareConcurrency || 4;
if (cores >= 8 && window.devicePixelRatio >= 2) {
tier = 'high';
} else {
tier = 'mid';
}
}
targetDoc.documentElement.dataset.tier = tier;
return tier;
}
function applyFontZoom(doc) {
var targetDoc = doc || document;
var savedZoom = localStorage.getItem('pageZoom');
if (savedZoom) {
var zoomVal = parseInt(savedZoom, 10);
if (!isNaN(zoomVal) && zoomVal >= 80 && zoomVal <= 130) {
targetDoc.documentElement.style.fontSize = ((zoomVal / 100) * 15) + 'px';
}
}
}
document.documentElement.dataset.tier = tier;
function initPreferences(doc) {
var targetDoc = doc || document;
var tier = applyTier(targetDoc);
applyFontZoom(targetDoc);
return tier;
}
initPreferences(document);
// Crucial for Astro ClientRouter: Apply to e.newDocument BEFORE swap to prevent attribute loss
document.addEventListener('astro:before-swap', function(e) {
if (e.newDocument) {
initPreferences(e.newDocument);
}
});
// Ensure Ultra-Fast Design & Zoom persist across ALL pages during client router transitions
document.addEventListener('astro:after-swap', function() {
var tier = initPreferences(document);
window.dispatchEvent(new CustomEvent('tier-change', { detail: { tier: tier } }));
});
document.addEventListener('astro:page-load', function() {
var tier = initPreferences(document);
window.dispatchEvent(new CustomEvent('tier-change', { detail: { tier: tier } }));
});
})();
</script>
@@ -144,16 +209,16 @@ const isRootPage = Astro.url.pathname === '/';
</script>
</head>
<body class="bg-background-custom text-foreground-custom bg-gradient-mesh min-h-screen flex flex-col selection:bg-primary-custom/30">
<Background client:load />
<div id="magnify-content" class="w-full min-h-screen flex flex-col pt-[60px] md:pt-0 md:pl-64">
<Background client:load transition:persist="main-bg" />
<div id="magnify-content" class="w-full max-w-full min-h-screen flex flex-col pt-[60px] md:pt-0 md:pl-64 overflow-x-clip">
<div class="hidden glass-card glass-nav text-glow glow-effect reveal-fade-up reveal-fade-down reveal-fade-left reveal-fade-right reveal-scale-up"></div>
<slot />
</div>
<BackToTop client:idle />
<PortfolioAssistantFAB client:idle />
<SpotlightLens client:load />
<NetworkStatus client:load />
<LagNotification client:load />
<BackToTop client:idle transition:persist="back-to-top" />
<PortfolioAssistantFAB client:idle transition:persist="portfolio-assistant" />
<SpotlightLens client:load transition:persist="spotlight-lens" />
<NetworkStatus client:load transition:persist="network-status" />
<LagNotification client:load transition:persist="lag-notification" />
<Analytics />
</body>
</html>
+1 -1
View File
@@ -18,7 +18,7 @@ const socialLinks = await getSocialLinks();
description="Learn more about Cris Charles Garcia's professional history, technical expertise, education, and developer biography."
image={settings.heroImage || settings.logoImage || '/logo.png'}
>
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
<main class="flex-1 pb-16">
<AboutDetails settings={settings} experience={experience} education={education} client:load />
+1 -1
View File
@@ -15,7 +15,7 @@ const socialLinks = await getSocialLinks();
description="Verified credentials and certifications earned by Cris Charles Garcia from Cisco and other industry-recognized programs."
image={settings.heroImage || settings.logoImage || '/logo.png'}
>
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
<main class="flex-1 min-h-screen pb-16">
<Certifications certifications={certifications} client:load />
+2 -2
View File
@@ -14,9 +14,9 @@ const socialLinks = await getSocialLinks();
description="Get in touch with Cris Charles Garcia for opportunities, consulting, or project collaborations."
image={settings.heroImage || settings.logoImage || '/logo.png'}
>
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
<main class="flex-1 pt-6 sm:pt-10 pb-16">
<main class="flex-1 pt-2 sm:pt-6 pb-16">
<ContactForm
socialLinks={socialLinks}
contactHeading={settings.contactHeading}
+2 -2
View File
@@ -15,9 +15,9 @@ const socialLinks = await getSocialLinks();
description="Visual showcase and photo archive of project milestones, developer activities, and academic experiences of Cris Charles Garcia."
image={settings.heroImage || settings.logoImage || '/logo.png'}
>
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
<main class="flex-1 min-h-screen pt-6 sm:pt-8 pb-16 px-4 sm:px-6">
<main class="flex-1 min-h-screen pt-3 sm:pt-8 pb-16 px-4 sm:px-6">
<div class="max-w-6xl mx-auto">
<PhotoGallery items={galleryItems} client:load />
</div>
+1 -1
View File
@@ -41,7 +41,7 @@ const socialLinks = await getSocialLinks();
image={settings.heroImage || settings.logoImage || '/logo.png'}
>
<!-- Navigation bar -->
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
<!-- Main sections -->
<main class="flex-1">
+3 -3
View File
@@ -15,10 +15,10 @@ const socialLinks = await getSocialLinks();
description="View all full-stack development and AI projects developed by Cris Charles Garcia."
image={settings.heroImage || settings.logoImage || '/logo.png'}
>
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
<main class="flex-1 pt-4 sm:pt-6 pb-16">
<div class="container mx-auto px-4 sm:px-6 max-w-5xl text-center mb-3 sm:mb-5">
<main class="flex-1 pt-2 sm:pt-6 pb-16">
<div class="container mx-auto px-4 sm:px-6 max-w-5xl text-center mb-2 sm:mb-5">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold font-heading tracking-tight text-foreground-custom">
All Work
</h1>
+3 -3
View File
@@ -25,9 +25,9 @@ const nextProject = currentIndex < allProjects.length - 1 ? allProjects[currentI
description={project.summary}
image={project.coverImage || settings.heroImage || settings.logoImage || '/logo.png'}
>
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath="/projects" client:load transition:persist="main-navbar" />
<main class="flex-1 pt-6 sm:pt-10 pb-20 relative overflow-hidden">
<main class="flex-1 pt-3 sm:pt-8 pb-20 relative overflow-hidden">
<!-- Subtle ambient background glows (no enclosing border) -->
<div class="absolute inset-0 z-0 overflow-hidden pointer-events-none">
<div class="absolute -top-32 left-1/3 w-[600px] h-[500px] rounded-full bg-primary-custom/5 blur-3xl"></div>
@@ -37,7 +37,7 @@ const nextProject = currentIndex < allProjects.length - 1 ? allProjects[currentI
<div class="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl relative z-10">
{/* Top Navigation & Meta Breadcrumb */}
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
<div class="flex flex-wrap items-center justify-between gap-4 mb-4 sm:mb-8">
<a
href="/projects"
class="inline-flex items-center gap-2 text-xs font-mono font-medium text-muted-foreground-custom hover:text-foreground-custom transition-colors group"
+138 -36
View File
@@ -20,11 +20,14 @@
--border-hover-color: rgba(37, 99, 235, 0.3);
--nav-bg: rgba(250, 250, 250, 0.8);
/* Minimalist Glass Surfaces (No thick borders or heavy background fills) */
--glass-bg: rgba(255, 255, 255, 0.55);
--glass-border: rgba(15, 17, 23, 0.05);
/* ─── Optimized Glassmorphism Tokens (Desktop / Base) ─── */
--glass-bg: rgba(255, 255, 255, 0.90);
--glass-border: rgba(15, 17, 23, 0.08);
--glass-blur: 16px;
--glass-shadow: 0 4px 20px -4px rgba(0, 0, 0, 0.03);
--glass-saturate: 140%;
--glass-shadow: 0 4px 24px -4px rgba(0, 0, 0, 0.06);
--glass-shadow-hover: 0 12px 32px -4px rgba(0, 0, 0, 0.1);
--glass-highlight: inset 0 1px 0 0 rgba(255, 255, 255, 0.85);
--bento-gap: 1.25rem;
--bento-radius: 24px;
@@ -42,26 +45,78 @@
--secondary-color: #38bdf8;
--muted-color: rgba(241, 245, 249, 0.04);
--muted-fg-color: #94a3b8;
--border-color: rgba(241, 245, 249, 0.06);
--border-hover-color: rgba(59, 130, 246, 0.35);
--nav-bg: rgba(9, 10, 15, 0.8);
--border-color: rgba(241, 245, 249, 0.08);
--border-hover-color: rgba(59, 130, 246, 0.4);
--nav-bg: rgba(9, 10, 15, 0.92);
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.06);
--glass-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.4);
/* ─── Optimized Dark Glass Surfaces (Crisp contrast over 3D background) ─── */
--glass-bg: rgba(13, 16, 26, 0.90);
--glass-border: rgba(255, 255, 255, 0.09);
--glass-blur: 16px;
--glass-saturate: 140%;
--glass-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.6);
--glass-shadow-hover: 0 16px 40px -6px rgba(0, 0, 0, 0.75);
--glass-highlight: inset 0 1px 0 0 rgba(255, 255, 255, 0.08);
--primary: 217 91% 60%;
--background: 230 25% 5%;
--foreground: 210 40% 98%;
}
/* ─── Device-Tiered Glassmorphism Optimization (Mobile & Tablet GPU Savings) ─── */
@media (max-width: 767px) {
:root {
--glass-blur: 10px;
--glass-saturate: 120%;
--glass-bg: rgba(255, 255, 255, 0.94);
--glass-shadow: 0 2px 14px -2px rgba(0, 0, 0, 0.05);
}
.dark {
--glass-blur: 10px;
--glass-saturate: 120%;
--glass-bg: rgba(13, 16, 26, 0.93);
--glass-shadow: 0 4px 20px -4px rgba(0, 0, 0, 0.5);
}
}
@media (min-width: 768px) and (max-width: 1023px) {
:root {
--glass-blur: 13px;
--glass-saturate: 130%;
}
.dark {
--glass-blur: 13px;
--glass-saturate: 130%;
}
}
html {
scrollbar-gutter: stable;
overflow-y: scroll;
background-color: var(--bg-color);
}
/* Smooth, flicker-free Astro View Transitions */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.15s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
html, body {
overflow-x: hidden;
max-width: 100vw;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
touch-action: manipulation;
}
body {
background-color: var(--bg-color);
color: var(--fg-color);
font-family: var(--font-body);
font-size: 15px;
line-height: 1.65;
overflow-x: hidden;
transition: background-color 0.25s ease, color 0.25s ease;
}
@@ -93,43 +148,86 @@
@variant dark (&:where(.dark, .dark *));
/* ─── Minimalist Glassmorphism Bento Cells ─── */
/* ─── High-Performance Glassmorphism (Calibrated across all devices) ─── */
.bento-card, .glass-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--bento-radius);
box-shadow: var(--glass-shadow);
transition: border-color 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
box-shadow: var(--glass-shadow), var(--glass-highlight);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
/* GPU Compositing layer (prevents full-screen repaints during 3D background scrolling) */
isolation: isolate;
transform: translate3d(0, 0, 0);
-webkit-transform: translate3d(0, 0, 0);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transition: border-color 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.25s ease, box-shadow 0.25s ease;
position: relative;
}
html[data-tier='mid'] .bento-card,
html[data-tier='high'] .bento-card,
html[data-tier='mid'] .glass-card,
html[data-tier='high'] .glass-card {
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
backdrop-filter: blur(var(--glass-blur)) saturate(140%);
/* Mouse Hover States (Desktop / pointer-capable only, prevents sticky hover on mobile touch) */
@media (hover: hover) and (pointer: fine) {
.bento-card:hover, .glass-card:hover {
border-color: var(--border-hover-color);
background: rgba(255, 255, 255, 0.96);
transform: translateY(-2px) translateZ(0);
box-shadow: var(--glass-shadow-hover), var(--glass-highlight);
}
.dark .bento-card:hover, .dark .glass-card:hover {
border-color: var(--border-hover-color);
background: rgba(18, 22, 36, 0.95);
transform: translateY(-2px) translateZ(0);
box-shadow: var(--glass-shadow-hover), var(--glass-highlight);
}
}
.bento-card:hover, .glass-card:hover {
border-color: var(--border-hover-color);
background: rgba(255, 255, 255, 0.7);
transform: translateY(-2px);
}
.dark .bento-card:hover, .dark .glass-card:hover {
background: rgba(255, 255, 255, 0.05);
/* Mobile & Touch Device Tap Feedback */
@media (hover: none) {
.bento-card:active, .glass-card:active {
transform: scale(0.995) translateZ(0);
transition: transform 0.1s ease;
}
}
/* Glass Navigation Bar */
.glass-nav {
background: var(--nav-bg);
border-bottom: 1px solid var(--glass-border);
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.12), var(--glass-highlight);
isolation: isolate;
transform: translate3d(0, 0, 0);
-webkit-transform: translate3d(0, 0, 0);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
}
html[data-tier='mid'] .glass-nav,
html[data-tier='high'] .glass-nav {
-webkit-backdrop-filter: blur(20px) saturate(160%);
backdrop-filter: blur(20px) saturate(160%);
/* Fallback for Browsers Without Backdrop-Filter Support */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
.bento-card, .glass-card {
background: #ffffff !important;
}
.dark .bento-card, .dark .glass-card {
background: #0f1118 !important;
}
.glass-nav {
background: #fafafa !important;
}
.dark .glass-nav {
background: #090a0f !important;
}
}
/* Accessibility: User Prefers Reduced Transparency */
@media (prefers-reduced-transparency: reduce) {
.bento-card, .glass-card, .glass-nav {
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
background: var(--bg-color) !important;
}
}
.bento-grid {
@@ -229,6 +327,10 @@ html[data-tier='low'] {
--glass-shadow: none !important;
}
html[data-tier='low'] canvas {
display: none !important;
}
/* 1. Global Performance & Complete Animation/Transition Stripping */
html[data-tier='low'] *,
html[data-tier='low'] *::before,
@@ -270,14 +372,14 @@ html[data-tier='low']:not(.dark) {
}
html[data-tier='low'].dark {
--bg-color: #0c0d12;
--fg-color: #f3f4f6;
--bg-color: #090a0f;
--fg-color: #f1f5f9;
--border-color: #374151;
--border-hover-color: #6b7280;
--muted-color: #1a1d26;
--muted-fg-color: #9ca3af;
--muted-fg-color: #94a3b8;
--primary-color: #3b82f6;
--nav-bg: #0c0d12;
--nav-bg: #090a0f;
}
html[data-tier='low'] body {