Fork of Chiri for Astro for my blog
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: transition logic

the3ash 32495d5b cf539f72

+102 -298
+29 -40
src/components/layout/TransitionWrapper.astro
··· 1 1 --- 2 - import { themeConfig } from '@/config' 3 2 import type { TransitionProps } from '@/types' 4 3 5 4 type Props = TransitionProps 6 5 7 6 const { type, class: className = '' } = Astro.props 8 - const fadeAnimation = themeConfig.general.fadeAnimation 9 7 const transitionName = type === 'post' ? 'post-content' : 'page-content' 10 8 --- 11 9 12 - { 13 - fadeAnimation ? ( 14 - <div transition:name={transitionName} transition:animate="initial" class={className}> 15 - <slot /> 16 - </div> 17 - ) : ( 18 - <div class={className}> 19 - <slot /> 20 - </div> 21 - ) 22 - } 10 + <div transition:name={transitionName} class={className} id="transition-wrapper"> 11 + <slot /> 12 + </div> 13 + 14 + <script is:inline> 15 + let isBackForward = false 16 + 17 + // Listen for back/forward navigation 18 + window.addEventListener('popstate', () => { 19 + isBackForward = true 20 + setTimeout(() => (isBackForward = false), 50) 21 + }) 22 + 23 + // Override startViewTransition to skip transitions for back/forward navigation 24 + const original = document.startViewTransition 25 + if (original) { 26 + document.startViewTransition = function (callback) { 27 + if (isBackForward) { 28 + callback?.() 29 + return { 30 + ready: Promise.resolve(), 31 + updateCallbackDone: Promise.resolve(), 32 + finished: Promise.resolve() 33 + } 34 + } 35 + return original.call(document, callback) 36 + } 37 + } 38 + </script> 23 39 24 40 <style is:global> 25 41 @supports (view-transition-name: none) { ··· 62 78 opacity: 1; 63 79 filter: blur(0); 64 80 transform: translateZ(0); 65 - } 66 - } 67 - 68 - .no-fade ::view-transition-old(post-content), 69 - .no-fade ::view-transition-new(post-content), 70 - .no-fade ::view-transition-old(page-content), 71 - .no-fade ::view-transition-new(page-content), 72 - .disable-transitions ::view-transition-old(post-content), 73 - .disable-transitions ::view-transition-new(post-content), 74 - .disable-transitions ::view-transition-old(page-content), 75 - .disable-transitions ::view-transition-new(page-content) { 76 - animation: none !important; 77 - } 78 - 79 - .reduce-motion ::view-transition-old(post-content), 80 - .reduce-motion ::view-transition-new(post-content), 81 - .reduce-motion ::view-transition-old(page-content), 82 - .reduce-motion ::view-transition-new(page-content) { 83 - animation: none !important; 84 - } 85 - 86 - @media (prefers-reduced-motion: reduce) { 87 - * { 88 - animation-duration: 0.01ms !important; 89 - transition-duration: 0.01ms !important; 90 - animation-iteration-count: 1 !important; 91 - scroll-behavior: auto !important; 92 81 } 93 82 } 94 83 </style>
+56 -84
src/components/ui/TableOfContents.astro
··· 48 48 let tocLinks = null 49 49 let headings = null 50 50 let titleLink = null 51 - // Add efficient Map lookup 52 51 const headingMap = new Map() 52 + let scrollTimeout = null 53 53 54 + // Initialize DOM cache 54 55 function initDOMCache() { 55 56 tocContainer = document.querySelector('.toc-container') 56 - tocLinks = document.querySelectorAll('.toc-link') 57 - headings = document.querySelectorAll('h1, h2, h3') 58 - titleLink = document.querySelector('.toc-link.toc-title') 59 - 60 - // Build heading map for efficient lookup 61 - buildHeadingMap() 62 - } 57 + if (!tocContainer) return false 63 58 64 - // Cache DOM queries to avoid repeated lookups 65 - function refreshDOMCache() { 66 59 tocLinks = document.querySelectorAll('.toc-link') 67 60 headings = document.querySelectorAll('h1, h2, h3') 68 61 titleLink = document.querySelector('.toc-link.toc-title') 69 62 buildHeadingMap() 63 + return true 70 64 } 71 65 66 + // Build heading map for efficient lookup 72 67 function buildHeadingMap() { 73 68 headingMap.clear() 74 69 if (!tocLinks) return ··· 82 77 }) 83 78 } 84 79 85 - // TOC positioning logic (similar to BackButton) 80 + // Calculate TOC positioning 86 81 function adjustTOCPosition() { 87 - if (!tocContainer) return 88 - 89 - // If not using centered layout, hide TOC 90 - if (!centeredLayout) { 91 - tocContainer.style.display = 'none' 82 + if (!tocContainer || !centeredLayout) { 83 + if (tocContainer) tocContainer.style.display = 'none' 92 84 return 93 85 } 94 86 95 - // Calculate available margin space for positioning 96 87 const pageWidth = window.innerWidth 97 88 const contentWidthValue = parseFloat(contentWidth) 98 - // Apply the same minimum width logic as in BaseLayout 99 89 const widthValue = Math.min(contentWidthValue, 50) 100 90 const shouldUseCustomWidth = widthValue > 25 101 91 const finalWidthValue = shouldUseCustomWidth ? widthValue : 25 102 92 const margin = (pageWidth - finalWidthValue * 16) / 2 103 93 const baseMinSpace = 11 * 16 // Base minimum space needed 104 - // If toc is enabled, need additional 2.5rem (40px) space 105 94 const minSpace = toc ? baseMinSpace + 40 : baseMinSpace 106 95 107 - // Show and position TOC fixed on the left if there's enough space 108 96 if (margin >= minSpace) { 109 97 tocContainer.style.display = 'block' 110 98 tocContainer.classList.add('fixed-position') 111 99 const basePosition = margin - baseMinSpace 112 - // If toc is enabled, move 2.5rem (40px) further left 113 100 const leftPosition = toc ? basePosition - 40 : basePosition 114 101 tocContainer.style.left = `${leftPosition}px` 115 102 116 103 // Update active state after TOC becomes visible 117 - setTimeout(() => updateActiveTOCItem(), 100) 104 + setTimeout(updateActiveTOCItem, 100) 118 105 } else { 119 106 tocContainer.style.display = 'none' 120 107 tocContainer.classList.remove('fixed-position') ··· 122 109 } 123 110 } 124 111 125 - // Hide TOC if there are no headings (only title) 112 + // Check if TOC should be visible based on content 126 113 function checkTOCVisibility() { 127 - // Use cached tocLinks if available, otherwise query once 128 - const tocItems = tocLinks 129 - ? Array.from(tocLinks).filter((link) => !link.classList.contains('toc-title')) 130 - : document.querySelectorAll('.toc-item:not(.toc-level-0)') 114 + if (!tocContainer || !tocLinks) return 131 115 132 - if (tocContainer && tocItems.length === 0) { 116 + const tocItems = Array.from(tocLinks).filter((link) => !link.classList.contains('toc-title')) 117 + if (tocItems.length === 0) { 133 118 tocContainer.style.display = 'none' 134 119 } 135 - } 136 - 137 - // Add click handlers to TOC links using event delegation 138 - function addTOCEventListeners() { 139 - if (!tocContainer) return 140 - 141 - // Remove existing event listener to prevent duplicates 142 - tocContainer.removeEventListener('click', handleTOCClick) 143 - 144 - // Use event delegation for better performance and reliability 145 - tocContainer.addEventListener('click', handleTOCClick) 146 120 } 147 121 148 122 // Handle TOC click events ··· 173 147 } 174 148 } 175 149 150 + // Add event listeners 151 + function addTOCEventListeners() { 152 + if (!tocContainer) return 153 + 154 + // Remove existing event listener to prevent duplicates 155 + tocContainer.removeEventListener('click', handleTOCClick) 156 + tocContainer.addEventListener('click', handleTOCClick) 157 + } 158 + 176 159 // Update active TOC item based on scroll position 177 160 function updateActiveTOCItem() { 178 - // Check if cache is valid, if not, re-fetch 179 161 if (!tocLinks || !headings || tocLinks.length === 0 || headings.length === 0) { 180 - refreshDOMCache() 162 + return 181 163 } 182 164 183 - if (!tocLinks || !headings) return 184 - 185 165 let currentActive = null 186 166 const scrollTop = window.pageYOffset + 100 // Offset for better detection 187 167 188 - // Check headings 168 + // Find the current active heading 189 169 headings.forEach((heading, index) => { 190 170 if (index === 0 && heading.tagName === 'H1') return 191 171 ··· 197 177 } 198 178 }) 199 179 200 - // Clear all active states first 201 - tocLinks.forEach((link) => { 202 - link.classList.remove('active') 203 - }) 180 + // Clear all active states 181 + tocLinks.forEach((link) => link.classList.remove('active')) 204 182 205 - // Only highlight a TOC item if we have a valid active heading 183 + // Set active state 206 184 if (currentActive && headingMap.has(currentActive)) { 207 185 const activeLink = headingMap.get(currentActive) 208 186 activeLink.classList.add('active') 209 - } else { 210 - // If no heading is active (e.g., at the very top of the page), 211 - // highlight "Back to top" to indicate we're at the beginning 212 - if (titleLink) { 213 - titleLink.classList.add('active') 214 - } 187 + } else if (titleLink) { 188 + titleLink.classList.add('active') 215 189 } 216 190 } 217 191 218 - // Initialize immediately to prevent layout shift 219 - initDOMCache() 220 - adjustTOCPosition() 221 - checkTOCVisibility() 222 - addTOCEventListeners() 223 - updateActiveTOCItem() // Set initial active state immediately 192 + // Initialize TOC with retry mechanism 193 + function initializeTOC() { 194 + if (!initDOMCache()) { 195 + // If TOC container doesn't exist yet, retry after a short delay 196 + setTimeout(initializeTOC, 100) 197 + return 198 + } 224 199 225 - // Listen for various page load events 226 - document.addEventListener('astro:page-load', () => { 227 - refreshDOMCache() 228 200 adjustTOCPosition() 201 + checkTOCVisibility() 229 202 addTOCEventListeners() 230 - updateActiveTOCItem() // Update active state after page load 203 + updateActiveTOCItem() 204 + } 205 + 206 + // Debounced scroll handler 207 + function handleScroll() { 208 + if (scrollTimeout) clearTimeout(scrollTimeout) 209 + scrollTimeout = setTimeout(updateActiveTOCItem, 16) // ~60fps 210 + } 211 + 212 + // Initialize with a small delay to ensure DOM is ready 213 + setTimeout(initializeTOC, 10) 214 + 215 + // Event listeners 216 + document.addEventListener('astro:page-load', () => { 217 + tocContainer = null // Reset container reference 218 + setTimeout(initializeTOC, 50) 231 219 }) 220 + 232 221 document.addEventListener('DOMContentLoaded', () => { 233 - // Only initialize if not already done by astro:page-load 234 - if (!tocContainer) { 235 - initDOMCache() 236 - adjustTOCPosition() 237 - addTOCEventListeners() 238 - updateActiveTOCItem() // Update active state after DOM ready 239 - } 222 + if (!tocContainer) initializeTOC() 240 223 }) 241 224 242 - // Re-initialize on resize and scroll 243 225 window.addEventListener('resize', adjustTOCPosition) 244 - 245 - // Add debounced scroll listener for better performance 246 - let scrollTimeout = null 247 - window.addEventListener('scroll', () => { 248 - if (scrollTimeout) { 249 - clearTimeout(scrollTimeout) 250 - } 251 - scrollTimeout = setTimeout(() => { 252 - updateActiveTOCItem() 253 - }, 16) // ~60fps, 16ms debounce 254 - }) 226 + window.addEventListener('scroll', handleScroll) 255 227 })() 256 228 </script> 257 229
-149
src/components/ui/TransitionInit.astro
··· 1 - --- 2 - import { themeConfig } from '@/config' 3 - 4 - const fadeAnimation = themeConfig.general.fadeAnimation 5 - --- 6 - 7 - {fadeAnimation && <meta name="view-transition" content="same-origin" />} 8 - 9 - <script is:inline define:vars={{ fadeAnimation }}> 10 - // Enhanced transition initialization - cache-resistant 11 - ;(() => { 12 - let isInitialized = false 13 - let initCounter = 0 14 - 15 - function initMotionPref(doc = document, force = false) { 16 - // Force re-initialization even if already done 17 - if (!force && isInitialized && initCounter < 3) { 18 - return 19 - } 20 - 21 - initCounter++ 22 - 23 - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches 24 - const supportsViewTransitions = 'startViewTransition' in document 25 - 26 - doc.documentElement.classList.toggle('reduce-motion', prefersReducedMotion) 27 - doc.documentElement.classList.toggle( 28 - 'disable-transitions', 29 - !fadeAnimation || !supportsViewTransitions 30 - ) 31 - 32 - // Dynamically control view transition navigation behavior 33 - let viewTransitionStyle = doc.getElementById('view-transition-style') 34 - 35 - if (fadeAnimation && supportsViewTransitions && !prefersReducedMotion) { 36 - // Enable view transition navigation 37 - if (!viewTransitionStyle) { 38 - viewTransitionStyle = doc.createElement('style') 39 - viewTransitionStyle.id = 'view-transition-style' 40 - viewTransitionStyle.textContent = '@view-transition { navigation: auto; }' 41 - doc.head.appendChild(viewTransitionStyle) 42 - } 43 - 44 - // Ensure HTML has correct transition attributes 45 - if (!doc.documentElement.hasAttribute('transition:animate')) { 46 - doc.documentElement.setAttribute('transition:animate', 'initial') 47 - } 48 - } else { 49 - // Disable view transition navigation 50 - if (viewTransitionStyle) { 51 - viewTransitionStyle.remove() 52 - } 53 - } 54 - 55 - doc.documentElement.classList.add('js') 56 - isInitialized = true 57 - } 58 - 59 - // Initialize motion preferences with force flag 60 - function initAll(doc = document, force = false) { 61 - initMotionPref(doc, force) 62 - } 63 - 64 - // Immediate initialization 65 - initAll(document, true) 66 - 67 - // Astro-specific events 68 - document.addEventListener('astro:before-swap', ({ newDocument }) => { 69 - initAll(newDocument, true) 70 - }) 71 - 72 - document.addEventListener('astro:page-load', () => { 73 - initAll(document, true) 74 - }) 75 - 76 - document.addEventListener('astro:after-swap', () => { 77 - initAll(document, true) 78 - }) 79 - 80 - // Browser navigation events 81 - window.addEventListener('pageshow', () => { 82 - // Always re-init on pageshow, especially when coming from cache 83 - initAll(document, true) 84 - }) 85 - 86 - window.addEventListener('pagehide', () => { 87 - // Reset state when page is hidden 88 - isInitialized = false 89 - initCounter = 0 90 - }) 91 - 92 - // Page visibility changes 93 - document.addEventListener('visibilitychange', () => { 94 - if (document.visibilityState === 'visible') { 95 - initAll(document, true) 96 - } 97 - }) 98 - 99 - // DOM ready states 100 - if (document.readyState === 'loading') { 101 - document.addEventListener('DOMContentLoaded', () => { 102 - initAll(document, true) 103 - }) 104 - } 105 - 106 - // Focus events (when user returns to tab) 107 - window.addEventListener('focus', () => { 108 - initAll(document, true) 109 - }) 110 - 111 - // Fallback: periodic check using requestIdleCallback with timeout-based control 112 - const startTime = Date.now() 113 - const timeoutDuration = 3000 // 3 seconds timeout 114 - let idleCallbackId = null 115 - 116 - function scheduleNextCheck() { 117 - const elapsed = Date.now() - startTime 118 - 119 - if (elapsed >= timeoutDuration) { 120 - return 121 - } 122 - 123 - initAll(document, true) 124 - 125 - // Schedule next check using requestIdleCallback if available, otherwise use requestAnimationFrame 126 - if ('requestIdleCallback' in window) { 127 - idleCallbackId = requestIdleCallback(scheduleNextCheck, { timeout: 1000 }) 128 - } else { 129 - idleCallbackId = requestAnimationFrame(() => { 130 - setTimeout(scheduleNextCheck, 500) 131 - }) 132 - } 133 - } 134 - 135 - // Start the fallback checks 136 - scheduleNextCheck() 137 - 138 - // Clean up when page unloads 139 - window.addEventListener('beforeunload', () => { 140 - if (idleCallbackId) { 141 - if ('requestIdleCallback' in window) { 142 - cancelIdleCallback(idleCallbackId) 143 - } else { 144 - cancelAnimationFrame(idleCallbackId) 145 - } 146 - } 147 - }) 148 - })() 149 - </script>
+14 -11
src/layouts/BaseLayout.astro
··· 1 1 --- 2 2 import { themeConfig } from '@/config' 3 + import { ClientRouter } from 'astro:transitions' 3 4 import ThemeManager from '@/components/ui/ThemeManager.astro' 4 - import TransitionInit from '@/components/ui/TransitionInit.astro' 5 5 import FaviconThemeSwitcher from '@/components/ui/FaviconThemeSwitcher.astro' 6 6 import TransitionWrapper from '@/components/layout/TransitionWrapper.astro' 7 7 import type { LayoutProps } from '@/types' ··· 17 17 const finalWidth = shouldUseCustomWidth ? `${widthValue}rem` : '25rem' 18 18 --- 19 19 20 - <html 21 - lang={language} 22 - class={fadeAnimation ? 'view-transitions-enabled' : 'view-transitions-disabled'} 23 - {...fadeAnimation ? { 'transition:animate': 'initial' } : {}} 24 - > 20 + <html lang={language}> 25 21 <head> 26 - <TransitionInit /> 22 + {fadeAnimation && <ClientRouter />} 27 23 <slot name="head" /> 28 24 </head> 29 25 <body 30 26 data-centered={themeConfig.general.centeredLayout} 31 - class={!fadeAnimation ? 'no-fade' : ''} 32 27 style={` 33 28 max-width: ${finalWidth}; 34 29 ${shouldUseCustomWidth ? `--content-width: ${widthValue}rem;` : ''} ··· 37 32 <ThemeManager /> 38 33 <FaviconThemeSwitcher /> 39 34 40 - <TransitionWrapper type={type} class="layout-wrapper"> 41 - <slot /> 42 - </TransitionWrapper> 35 + { 36 + fadeAnimation ? ( 37 + <TransitionWrapper type={type} class="layout-wrapper"> 38 + <slot /> 39 + </TransitionWrapper> 40 + ) : ( 41 + <div class="layout-wrapper"> 42 + <slot /> 43 + </div> 44 + ) 45 + } 43 46 </body> 44 47 </html> 45 48
+3 -14
src/styles/global.css
··· 99 99 100 100 html { 101 101 scrollbar-gutter: stable; 102 + overscroll-behavior-y: contain; 103 + -webkit-overflow-scrolling: touch; 104 + scroll-behavior: smooth; 102 105 } 103 106 104 107 body { ··· 126 129 body { 127 130 padding: 4rem 1.25rem 1.25rem 1.25rem; 128 131 } 129 - } 130 - 131 - html { 132 - overscroll-behavior-y: contain; 133 - -webkit-overflow-scrolling: touch; 134 - scroll-behavior: smooth; 135 - } 136 - 137 - html.view-transitions-enabled { 138 - view-transition-name: enabled; 139 - } 140 - 141 - html.view-transitions-disabled { 142 - view-transition-name: none; 143 132 } 144 133 145 134 @media (prefers-reduced-motion: reduce) {