From 8c1f34b92c28545474fd9b53b4f13a1469af94ce Mon Sep 17 00:00:00 2001 From: Melendez Date: Sat, 8 Aug 2026 16:02:27 +0800 Subject: [PATCH 1/3] Edit nav indicator, responsive footer & dropdowns - Introduce a sliding .nav-indicator (HTML/CSS/JS) that animates under the active nav link. - Refactor theme and language dropdown logic to support multiple instances, add touch-friendly toggle behavior (open/close all), and ensure theme icons and selected states update across selectors. - Add footer controls (theme + language) for narrow screens and make the About logo static on small screens. Minor CSS/behavior tweaks: simplify nav hover styles, responsive layout adjustments, and prevent logo fly animation on narrow viewports. --- Web/css/index.css | 73 ++++++++++++++++----- Web/index.html | 53 +++++++++++++-- Web/src/script.js | 163 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 227 insertions(+), 62 deletions(-) diff --git a/Web/css/index.css b/Web/css/index.css index 14d8282..4146bf8 100644 --- a/Web/css/index.css +++ b/Web/css/index.css @@ -106,6 +106,7 @@ nav.about-active ul { align-items: center; gap: 0.5rem; height: 100%; + margin-left: auto; } nav ul { @@ -138,26 +139,28 @@ nav a { transition: color 0.3s ease; } -nav a::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 0; - height: 2px; - background: var(--primary-color); - transition: width 0.3s ease; -} - -nav a:hover::after, -nav a.active::after { - width: 100%; +nav a:hover { + color: var(--primary-color); } nav a.active { color: var(--primary-color); } +.nav-indicator { + position: absolute; + height: 3px; + border-radius: 999px; + background: var(--primary-color); + left: 0; + top: 0; + width: 0; + pointer-events: none; + transition: left 0.35s cubic-bezier(0.16, 1, 0.3, 1), + top 0.35s cubic-bezier(0.16, 1, 0.3, 1), + width 0.35s cubic-bezier(0.16, 1, 0.3, 1); +} + .theme-toggle, .lang-toggle { display: flex; @@ -1040,6 +1043,13 @@ main section { justify-content: center; } +.about-logo { + display: none; + max-width: 260px; + width: 100%; + height: auto; +} + .about-content { flex: 1 1 320px; max-width: 440px; @@ -1079,6 +1089,22 @@ footer { scroll-snap-align: end; } +.footer-controls { + display: none; + justify-content: center; + align-items: center; + gap: 1.5rem; + margin-bottom: 1rem; +} + +.footer-controls .theme-selector { + height: auto; +} + +.footer-controls .lang-selector { + height: auto; +} + /* ===== Theme Selector ===== */ .theme-selector { position: relative; @@ -1090,6 +1116,13 @@ footer { transform: translateY(0) scale(1); } +.theme-dropdown.show, +.lang-dropdown.show { + opacity: 1; + visibility: visible; + transform: translateY(0) scale(1); +} + .theme-dropdown button, .lang-dropdown button { display: flex; @@ -1210,13 +1243,21 @@ footer { /* ===== Responsive ===== */ @media (max-width: 768px) { nav { - width: calc(100% - 1rem); + display: none; } - nav ul { + .nav-indicator { display: none; } + .about-logo { + display: block; + } + + .footer-controls { + display: flex; + } + .hero h1 { font-size: 3rem; } diff --git a/Web/index.html b/Web/index.html index c07fc10..f874cd4 100644 --- a/Web/index.html +++ b/Web/index.html @@ -51,6 +51,7 @@ About +
- +
+
+ + + +
+
+
+ +
+ + + + +
+
+

© 2024 Known. { // ===== Theme System ===== - const themeDropdown = document.querySelector('.theme-dropdown'); - function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' @@ -17,44 +15,54 @@ document.addEventListener('DOMContentLoaded', () => { } function updateThemeIcon(theme) { - const darkIcon = document.querySelector('.dark-icon'); - const lightIcon = document.querySelector('.light-icon'); - const systemIcon = document.querySelector('.system-icon'); - - darkIcon.style.display = 'none'; - lightIcon.style.display = 'none'; - systemIcon.style.display = 'none'; - - if (theme === 'system') { - systemIcon.style.display = 'inline-block'; - } else if (theme === 'dark') { - darkIcon.style.display = 'inline-block'; - } else { - lightIcon.style.display = 'inline-block'; - } + document.querySelectorAll('.theme-selector').forEach((selector) => { + const icons = { + dark: selector.querySelector('.dark-icon'), + light: selector.querySelector('.light-icon'), + system: selector.querySelector('.system-icon'), + }; + + if (theme === 'system') { + icons.system.style.display = 'inline-block'; + icons.dark.style.display = 'none'; + icons.light.style.display = 'none'; + } else if (theme === 'dark') { + icons.dark.style.display = 'inline-block'; + icons.light.style.display = 'none'; + icons.system.style.display = 'none'; + } else { + icons.light.style.display = 'inline-block'; + icons.dark.style.display = 'none'; + icons.system.style.display = 'none'; + } + }); } function updateSelectedTheme(theme) { - document.querySelectorAll('.theme-dropdown button').forEach((btn) => { - btn.removeAttribute('data-selected'); + document.querySelectorAll('.theme-dropdown').forEach((dropdown) => { + dropdown.querySelectorAll('button').forEach((btn) => { + btn.removeAttribute('data-selected'); + }); + const selectedButton = dropdown.querySelector( + `button[data-theme="${theme}"]`, + ); + if (selectedButton) { + selectedButton.setAttribute('data-selected', 'true'); + } }); - const selectedButton = document.querySelector( - `.theme-dropdown button[data-theme="${theme}"]`, - ); - if (selectedButton) { - selectedButton.setAttribute('data-selected', 'true'); - } } const savedTheme = localStorage.getItem('theme') || 'system'; applyTheme(savedTheme); - themeDropdown.addEventListener('click', (e) => { - const button = e.target.closest('button'); - if (!button) return; - const theme = button.dataset.theme; - localStorage.setItem('theme', theme); - applyTheme(theme); + document.querySelectorAll('.theme-dropdown').forEach((dropdown) => { + dropdown.addEventListener('click', (e) => { + const button = e.target.closest('button'); + if (!button) return; + const theme = button.dataset.theme; + localStorage.setItem('theme', theme); + applyTheme(theme); + }); }); window @@ -68,22 +76,56 @@ document.addEventListener('DOMContentLoaded', () => { }); // ===== Language Selection ===== - const langDropdown = document.querySelector('.lang-dropdown'); const savedLang = localStorage.getItem('lang') || 'zh'; updateSelectedLang(savedLang); function updateSelectedLang(lang) { - document.querySelectorAll('.lang-dropdown button').forEach((btn) => { - btn.removeAttribute('data-selected'); + document.querySelectorAll('.lang-dropdown').forEach((dropdown) => { + dropdown.querySelectorAll('button').forEach((btn) => { + btn.removeAttribute('data-selected'); + }); + const selectedButton = dropdown.querySelector( + `button[data-lang="${lang}"]`, + ); + if (selectedButton) { + selectedButton.setAttribute('data-selected', 'true'); + } }); - const selectedButton = document.querySelector( - `.lang-dropdown button[data-lang="${lang}"]`, - ); - if (selectedButton) { - selectedButton.setAttribute('data-selected', 'true'); - } } + // ===== Dropdown Toggle (touch-friendly) ===== + function closeAllDropdowns() { + document + .querySelectorAll('.theme-dropdown, .lang-dropdown') + .forEach((dropdown) => dropdown.classList.remove('show')); + } + + document.querySelectorAll('.theme-toggle').forEach((toggle) => { + toggle.addEventListener('click', (e) => { + e.stopPropagation(); + const dropdown = toggle + .closest('.theme-selector') + .querySelector('.theme-dropdown'); + const wasOpen = dropdown.classList.contains('show'); + closeAllDropdowns(); + if (!wasOpen) dropdown.classList.add('show'); + }); + }); + + document.querySelectorAll('.lang-toggle').forEach((toggle) => { + toggle.addEventListener('click', (e) => { + e.stopPropagation(); + const dropdown = toggle + .closest('.lang-selector') + .querySelector('.lang-dropdown'); + const wasOpen = dropdown.classList.contains('show'); + closeAllDropdowns(); + if (!wasOpen) dropdown.classList.add('show'); + }); + }); + + document.addEventListener('click', closeAllDropdowns); + // ===== Particle System ===== function createParticles() { const container = document.getElementById('particles'); @@ -166,6 +208,7 @@ document.addEventListener('DOMContentLoaded', () => { // ===== Nav Shadow on Scroll ===== const nav = document.querySelector('nav'); + const navIndicator = document.querySelector('.nav-indicator'); function updateNavStyle() { if (window.scrollY > 50) { @@ -175,6 +218,22 @@ document.addEventListener('DOMContentLoaded', () => { } } + // ===== Sliding Nav Indicator ===== + function updateNavIndicator() { + if (!navIndicator || !nav) return; + const activeLink = document.querySelector('nav ul .nav-link.active'); + if (!activeLink) { + navIndicator.style.opacity = '0'; + return; + } + const navRect = nav.getBoundingClientRect(); + const linkRect = activeLink.getBoundingClientRect(); + navIndicator.style.opacity = '1'; + navIndicator.style.left = linkRect.left - navRect.left + 'px'; + navIndicator.style.top = linkRect.bottom - navRect.top + 6 + 'px'; + navIndicator.style.width = linkRect.width + 'px'; + } + // ===== Floating Logo: Fly to About description ===== const aboutSection = document.getElementById('about'); const aboutLogoColumn = @@ -185,6 +244,7 @@ document.addEventListener('DOMContentLoaded', () => { const LOGO_FLY_DURATION = 600; let logoFlying = false; let logoTrackReady = false; + const isNarrowScreen = () => window.matchMedia('(max-width: 768px)').matches; function isAboutActive() { if (!aboutSection) return false; @@ -255,7 +315,22 @@ document.addEventListener('DOMContentLoaded', () => { logo.classList.remove('logo-about-large'); const currentLeft = parseFloat(logo.style.left) || 0; const currentTop = parseFloat(logo.style.top) || 0; - const slot = logoSlotViewport(); + // Measure the target (expanded) nav slot without a visual flash + const prevTransition = nav.style.transition; + nav.style.transition = 'none'; + nav.classList.remove('about-active'); + const expandedNavRect = nav.getBoundingClientRect(); + nav.classList.add('about-active'); + nav.getBoundingClientRect(); // re-layout at shrunk size so the expand transition can replay + nav.style.transition = prevTransition; + const padLeft = parseFloat(getComputedStyle(nav).paddingLeft) || 0; + const slot = { + left: expandedNavRect.left + padLeft, + top: + expandedNavRect.top + (expandedNavRect.height - logo.offsetHeight) / 2, + }; + // Expand the bar while the logo flies back + nav.classList.remove('about-active'); const anim = logo.animate( [ { left: currentLeft + 'px', top: currentTop + 'px' }, @@ -270,7 +345,6 @@ document.addEventListener('DOMContentLoaded', () => { anim.onfinish = () => { anim.cancel(); nav.prepend(logo); - nav.classList.remove('about-active'); logo.classList.remove('logo-fixed'); logo.style.left = ''; logo.style.top = ''; @@ -279,6 +353,8 @@ document.addEventListener('DOMContentLoaded', () => { function updateAboutLogo() { if (!aboutSection || !aboutLogoColumn || !aboutContent || !logo) return; + // Logo lives statically in the About section on narrow screens + if (isNarrowScreen()) return; if (isAboutActive()) { if (!logoFlying) { flyLogoToAbout(); @@ -347,6 +423,7 @@ document.addEventListener('DOMContentLoaded', () => { window.requestAnimationFrame(() => { updateActiveNav(); updateNavStyle(); + updateNavIndicator(); updateParallax(); updateAboutLogo(); ticking = false; @@ -358,9 +435,11 @@ document.addEventListener('DOMContentLoaded', () => { // Initial calls updateActiveNav(); updateNavStyle(); + updateNavIndicator(); updateAboutLogo(); window.addEventListener('resize', () => { + updateNavIndicator(); if (logoFlying) { cancelLogoAnimations(); logoTrackReady = true; From 8fecf2557049c832d17bd4477cff1576af85d6d4 Mon Sep 17 00:00:00 2001 From: Melendez Date: Sat, 8 Aug 2026 16:14:40 +0800 Subject: [PATCH 2/3] Revamp 404 page with theme, language, particles Redesign 404 page. --- Web/404.html | 224 ++++++++++++++++++++++-- Web/css/404.css | 449 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 552 insertions(+), 121 deletions(-) diff --git a/Web/404.html b/Web/404.html index 4c0c82e..47ead8e 100644 --- a/Web/404.html +++ b/Web/404.html @@ -1,27 +1,217 @@ - + - - - 404 - - + + + 404 - Known + + + + -

-