merge in new changes
This commit is contained in:
		
							
								
								
									
										85
									
								
								src/components/ui/buttons/Bookmark.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/components/ui/buttons/Bookmark.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| --- | ||||
| import Icon from '@components/ui/icons/icon.astro'; | ||||
| --- | ||||
|  | ||||
| <button | ||||
|   type="button" | ||||
|   class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700" | ||||
|   data-bookmark-button="bookmark-button" | ||||
| > | ||||
|   <Icon name="bookmark" /> | ||||
| </button> | ||||
|  | ||||
| <script> | ||||
|   class Bookmark { | ||||
|     private static readonly BOOKMARKS_KEY = 'bookmarks'; | ||||
|     private bookmarkButton: Element | null; | ||||
|  | ||||
|     constructor(private dataAttrValue: string) { | ||||
|       this.bookmarkButton = document.querySelector(`[data-bookmark-button="${dataAttrValue}"]`); | ||||
|     } | ||||
|  | ||||
|     private getStoredBookmarks(): string[] { | ||||
|       const item = localStorage.getItem(Bookmark.BOOKMARKS_KEY); | ||||
|       return item ? JSON.parse(item) : []; | ||||
|     } | ||||
|  | ||||
|     init(): void { | ||||
|       if (this.bookmarkButton && this.isStored()) { | ||||
|         this.markAsStored(); | ||||
|       } | ||||
|  | ||||
|       this.bookmarkButton?.addEventListener('click', () => this.toggleBookmark()); | ||||
|     } | ||||
|  | ||||
|     isStored(): boolean { | ||||
|       return this.getStoredBookmarks().includes(window.location.pathname); | ||||
|     } | ||||
|  | ||||
|     markAsStored(): void { | ||||
|       if (this.bookmarkButton) { | ||||
|         this.bookmarkButton.classList.add('bookmarked'); | ||||
|         const svgElement = this.bookmarkButton.querySelector('svg'); | ||||
|         if (svgElement) { | ||||
|           svgElement.setAttribute('class', 'h-6 w-6 fill-red-500 dark:fill-red-500'); | ||||
|         } | ||||
|         const pathElement = svgElement?.querySelector('path'); | ||||
|         if (pathElement) { | ||||
|           pathElement.setAttribute('class', 'fill-current text-red-500 dark:text-red-500'); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     unmarkAsStored(): void { | ||||
|       if (this.bookmarkButton) { | ||||
|         this.bookmarkButton.classList.remove('bookmarked'); | ||||
|         const svgElement = this.bookmarkButton.querySelector('svg'); | ||||
|         if (svgElement) { | ||||
|           svgElement.setAttribute('class', 'h-6 w-6 fill-none'); | ||||
|         } | ||||
|         const pathElement = svgElement?.querySelector('path'); | ||||
|         if (pathElement) { | ||||
|           pathElement.setAttribute( | ||||
|             'class', | ||||
|             'fill-current text-neutral-500 group-hover:text-red-400 dark:text-neutral-500 group-hover:dark:text-red-400' | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     toggleBookmark(): void { | ||||
|       const storedBookmarks = this.getStoredBookmarks(); | ||||
|       const index = storedBookmarks.indexOf(window.location.pathname); | ||||
|       if (index !== -1) { | ||||
|         storedBookmarks.splice(index, 1); | ||||
|         this.unmarkAsStored(); | ||||
|       } else { | ||||
|         storedBookmarks.push(window.location.pathname); | ||||
|         this.markAsStored(); | ||||
|       } | ||||
|       localStorage.setItem(Bookmark.BOOKMARKS_KEY, JSON.stringify(storedBookmarks)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   new Bookmark('bookmark-button').init(); | ||||
| </script> | ||||
							
								
								
									
										32
									
								
								src/components/ui/buttons/GiteaBtn.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/components/ui/buttons/GiteaBtn.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| --- | ||||
| import { Icon } from 'astro-icon/components'; | ||||
|  | ||||
| const { title, url } = Astro.props; | ||||
|  | ||||
| interface Props { | ||||
|   title?: string; | ||||
|   url?: string; | ||||
| } | ||||
|  | ||||
| const baseClasses = | ||||
|   'group group-hover inline-flex items-center justify-center gap-x-3 rounded-full px-4 py-3 text-center text-sm font-medium text-neutral-200'; | ||||
| const borderClasses = 'border border-transparent'; | ||||
| const bgColorClasses = | ||||
|   'bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary'; | ||||
| const shadowClasses = 'shadow-sm'; | ||||
| const fontSizeClasses = '2xl:text-base'; | ||||
| --- | ||||
|  | ||||
| <a | ||||
|   class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${shadowClasses} ${fontSizeClasses} `} | ||||
|   href={url} | ||||
|   target="_blank" | ||||
|   rel="noopener noreferrer" | ||||
| > | ||||
|   <Icon name="pajamas:gitea" class="h-4 w-4 md:h-6 md:w-6" /> | ||||
|   {title} | ||||
|   <Icon | ||||
|     name="mdi:keyboard-arrow-right" | ||||
|     class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5" | ||||
|   /> | ||||
| </a> | ||||
							
								
								
									
										35
									
								
								src/components/ui/buttons/GoBack.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/components/ui/buttons/GoBack.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| --- | ||||
| import Icon from '@components/ui/icons/icon.astro'; | ||||
|  | ||||
| const { title, noArrow } = Astro.props; | ||||
|  | ||||
| interface Props { | ||||
|   title?: string; | ||||
|   url?: string; | ||||
|   noArrow?: boolean; | ||||
|   addHome?: boolean; | ||||
| } | ||||
|  | ||||
| const baseClasses = | ||||
|   'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-neutral-500 transition duration-300 focus-visible:ring outline-none'; | ||||
| const borderClasses = 'border border-transparent'; | ||||
| const bgColorClasses = 'bg-steel hover:bg-sky-800 active:bg-orange-500 dark:focus:outline-none'; | ||||
| const disableClasses = 'disabled:pointer-events-none disabled:opacity-50'; | ||||
| const fontSizeClasses = '2xl:text-base'; | ||||
| const ringClasses = 'dark:ring-neutral-200'; | ||||
| --- | ||||
|  | ||||
| <button | ||||
|   class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`} | ||||
|   id="back-button" | ||||
|   data-astro-prefetch | ||||
| > | ||||
|   {noArrow ? null : <Icon name="arrowLeft" />} | ||||
|   {title} | ||||
| </button> | ||||
|  | ||||
| <script> | ||||
|   document.getElementById('back-button')?.addEventListener('click', () => { | ||||
|     window.history.back(); | ||||
|   }); | ||||
| </script> | ||||
							
								
								
									
										45
									
								
								src/components/ui/buttons/PrimaryCTA.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/components/ui/buttons/PrimaryCTA.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| --- | ||||
| import { Icon } from 'astro-icon/components'; | ||||
|  | ||||
| const { title, url, noArrow, addHome, addClass } = Astro.props; | ||||
|  | ||||
| interface Props { | ||||
|   title?: string; | ||||
|   url?: string; | ||||
|   noArrow?: boolean; | ||||
|   addHome?: boolean; | ||||
|   addClass?: string; | ||||
| } | ||||
|  | ||||
| const baseClasses = | ||||
|   'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-100  transition duration-300 '; | ||||
| const borderClasses = 'border border-transparent'; | ||||
| const bgColorClasses = 'bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda'; | ||||
| const disableClasses = 'disabled:pointer-events-none disabled:opacity-50'; | ||||
| const fontSizeClasses = '2xl:text-base'; | ||||
| const ringClasses = 'dark:ring-neutral-200'; | ||||
| --- | ||||
|  | ||||
| <a | ||||
|   class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${addClass}`} | ||||
|   href={url} | ||||
|   data-astro-prefetch | ||||
| > | ||||
|   { | ||||
|     addHome ? ( | ||||
|       <Icon | ||||
|         name="mdi:home-variant-outline" | ||||
|         class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5" | ||||
|       /> | ||||
|     ) : null | ||||
|   } | ||||
|   {title} | ||||
|   { | ||||
|     noArrow ? null : ( | ||||
|       <Icon | ||||
|         name="mdi:keyboard-arrow-right" | ||||
|         class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5" | ||||
|       /> | ||||
|     ) | ||||
|   } | ||||
| </a> | ||||
							
								
								
									
										26
									
								
								src/components/ui/buttons/SecondaryCTA.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/components/ui/buttons/SecondaryCTA.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| --- | ||||
| const { title, url } = Astro.props; | ||||
|  | ||||
| interface Props { | ||||
|   title?: string; | ||||
|   url?: string; | ||||
| } | ||||
|  | ||||
| const baseClasses = | ||||
|   'inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-600 shadow-sm outline-none ring-neutral-500 focus-visible:ring transition duration-300'; | ||||
| const borderClasses = 'border border-neutral-200'; | ||||
| const bgColorClasses = 'bg-neutral-300'; | ||||
| const hoverClasses = 'hover:bg-neutral-400/50 hover:text-neutral-600 active:text-neutral-700'; | ||||
| const disableClasses = 'disabled:pointer-events-none disabled:opacity-50'; | ||||
| const fontSizeClasses = '2xl:text-base'; | ||||
| const ringClasses = 'ring-neutral-500'; | ||||
| const darkClasses = | ||||
|   'dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-300 dark:ring-neutral-200 dark:hover:bg-neutral-600 dark:focus:outline-none'; | ||||
| --- | ||||
|  | ||||
| <a | ||||
|   class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${darkClasses}`} | ||||
|   href={url} | ||||
| > | ||||
|   {title} | ||||
| </a> | ||||
							
								
								
									
										150
									
								
								src/components/ui/buttons/SocialShare.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/components/ui/buttons/SocialShare.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| --- | ||||
| import Icon from '@components/ui/icons/icon.astro'; | ||||
|  | ||||
| const { pageTitle, title = 'Share' } = Astro.props; | ||||
|  | ||||
| interface Props { | ||||
|   pageTitle: string; | ||||
|   title?: string; | ||||
| } | ||||
|  | ||||
| type SocialPlatform = { | ||||
|   name: string; | ||||
|   url: string; | ||||
|   svg: string; | ||||
| }; | ||||
|  | ||||
| const socialPlatforms: SocialPlatform[] = [ | ||||
|   { | ||||
|     name: 'Facebook', | ||||
|     url: `https://www.facebook.com/share.php?u=${Astro.url}&title=${pageTitle}`, | ||||
|     svg: 'facebook', | ||||
|   }, | ||||
|   { | ||||
|     name: 'X', | ||||
|     url: `https://twitter.com/home/?status=${pageTitle}${Astro.url}`, | ||||
|     svg: 'x', | ||||
|   }, | ||||
|   { | ||||
|     name: 'LinkedIn', | ||||
|     url: `https://www.linkedin.com/shareArticle?mini=true&url=${Astro.url}&title=${pageTitle}`, | ||||
|     svg: 'linkedIn', | ||||
|   }, | ||||
| ]; | ||||
| --- | ||||
|  | ||||
| <div class="hs-dropdown relative inline-flex [--auto-close:inside] [--placement:top-left]"> | ||||
|   <button | ||||
|     id="hs-dropup" | ||||
|     type="button" | ||||
|     class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 hover:text-neutral-700 focus-visible:ring dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:outline-none" | ||||
|   > | ||||
|     <Icon name="share" /> | ||||
|  | ||||
|     {title} | ||||
|   </button> | ||||
|  | ||||
|   <div | ||||
|     class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 z-10 hidden w-72 divide-y divide-neutral-200 rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800" | ||||
|     aria-labelledby="hs-dropup" | ||||
|   > | ||||
|     <div class="py-2 first:pt-0 last:pb-0"> | ||||
|       { | ||||
|         socialPlatforms.map((platform) => ( | ||||
|           <a | ||||
|             class="flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700" | ||||
|             href={platform.url} | ||||
|           > | ||||
|             <Icon name={platform.svg} /> | ||||
|             Share on {platform.name} | ||||
|           </a> | ||||
|         )) | ||||
|       } | ||||
|     </div> | ||||
|     <div class="py-2 first:pt-0 last:pb-0"> | ||||
|       <button | ||||
|         type="button" | ||||
|         class="js-clipboard hover:text-dark focus-visible:ring-secondary group inline-flex w-full items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700" | ||||
|         data-clipboard-success-text="Copied" | ||||
|       > | ||||
|         <svg | ||||
|           class="js-clipboard-default h-4 w-4 transition group-hover:rotate-6" | ||||
|           width="24" | ||||
|           height="24" | ||||
|           viewBox="0 0 24 24" | ||||
|           fill="none" | ||||
|           stroke="currentColor" | ||||
|           stroke-width="2" | ||||
|           stroke-linecap="round" | ||||
|           stroke-linejoin="round" | ||||
|         > | ||||
|           <rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect> | ||||
|           <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path> | ||||
|         </svg> | ||||
|  | ||||
|         <svg | ||||
|           class="js-clipboard-success hidden h-4 w-4 text-neutral-700 dark:text-neutral-300" | ||||
|           width="24" | ||||
|           height="24" | ||||
|           viewBox="0 0 24 24" | ||||
|           fill="none" | ||||
|           stroke="currentColor" | ||||
|           stroke-width="2" | ||||
|           stroke-linecap="round" | ||||
|           stroke-linejoin="round" | ||||
|         > | ||||
|           <polyline points="20 6 9 17 4 12"></polyline> | ||||
|         </svg> | ||||
|         <span class="js-clipboard-success-text">Copy link</span> | ||||
|       </button> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <!--Import the necessary Dropdown and Clipboard plugins--> | ||||
| <!--https://preline.co/plugins/html/dropdown.html--> | ||||
| <!--<script is:inline src="/scripts/vendor/preline/dropdown/index.js"></script>--> | ||||
|  | ||||
| <!-- https://clipboardjs.com/ --> | ||||
| <!--<script is:inline src="/scripts/vendor/clipboard.min.js"></script>--> | ||||
|  | ||||
| <script is:inline> | ||||
|   (function () { | ||||
|     window.addEventListener('load', () => { | ||||
|       const $clipboards = document.querySelectorAll('.js-clipboard'); | ||||
|       $clipboards.forEach((el) => { | ||||
|         const clipboard = new ClipboardJS(el, { | ||||
|           text: () => { | ||||
|             return window.location.href; | ||||
|           }, | ||||
|         }); | ||||
|         clipboard.on('success', () => { | ||||
|           const $default = el.querySelector('.js-clipboard-default'); | ||||
|           const $success = el.querySelector('.js-clipboard-success'); | ||||
|           const $successText = el.querySelector('.js-clipboard-success-text'); | ||||
|           const successText = el.dataset.clipboardSuccessText || ''; | ||||
|           let oldSuccessText; | ||||
|  | ||||
|           if ($successText) { | ||||
|             oldSuccessText = $successText.textContent; | ||||
|             $successText.textContent = successText; | ||||
|           } | ||||
|           if ($default && $success) { | ||||
|             $default.style.display = 'none'; | ||||
|             $success.style.display = 'block'; | ||||
|           } | ||||
|  | ||||
|           setTimeout(() => { | ||||
|             if ($successText && oldSuccessText) { | ||||
|               $successText.textContent = oldSuccessText; | ||||
|             } | ||||
|             if ($default && $success) { | ||||
|               $success.style.display = ''; | ||||
|               $default.style.display = ''; | ||||
|             } | ||||
|           }, 800); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   })(); | ||||
| </script> | ||||
							
								
								
									
										279
									
								
								src/components/ui/buttons/ThemeToggle.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								src/components/ui/buttons/ThemeToggle.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | ||||
| --- | ||||
|  | ||||
| --- | ||||
|  | ||||
| <button | ||||
|   id="theme-toggle" | ||||
|   data-theme-toggle | ||||
|   class="group dark:hover:bg-steel/30 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-yellow-300/20 focus:outline-hidden sm:p-2" | ||||
|   aria-label="Toggle dark mode" | ||||
| > | ||||
|   <div class="relative z-10 flex h-5 w-5 items-center justify-center"> | ||||
|     <!-- Sun icon --> | ||||
|     <svg | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-600 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-400" | ||||
|       viewBox="0 0 24 24" | ||||
|       fill="none" | ||||
|       stroke="currentColor" | ||||
|       stroke-width="2" | ||||
|       stroke-linecap="round" | ||||
|       stroke-linejoin="round" | ||||
|     > | ||||
|       <circle cx="12" cy="12" r="5"></circle> | ||||
|       <path | ||||
|         d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" | ||||
|       ></path> | ||||
|     </svg> | ||||
|  | ||||
|     <!-- Moon icon --> | ||||
|     <svg | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-600 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-400" | ||||
|       viewBox="0 0 24 24" | ||||
|       fill="none" | ||||
|       stroke="currentColor" | ||||
|       stroke-width="2" | ||||
|       stroke-linecap="round" | ||||
|       stroke-linejoin="round" | ||||
|     > | ||||
|       <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> | ||||
|     </svg> | ||||
|   </div> | ||||
| </button> | ||||
|  | ||||
| <script is:inline> | ||||
|   // Use a function to persist theme when using SPA transitions | ||||
|   // https://docs.astro.build/en/guides/view-transitions/#script-re-execution | ||||
|   function applyTheme() { | ||||
|     localStorage.theme === 'dark' | ||||
|       ? document.documentElement.classList.add('dark') | ||||
|       : document.documentElement.classList.remove('dark'); | ||||
|   } | ||||
|  | ||||
|   document.addEventListener('astro:after-swap', applyTheme); | ||||
|  | ||||
|   applyTheme(); | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
|   // Use a function to handle theme toggle to ensure it can be called from anywhere | ||||
|   function setupThemeToggle() { | ||||
|     const themeToggles = document.querySelectorAll('[data-theme-toggle]'); | ||||
|  | ||||
|     // Create theme switch overlay element if it doesn't exist | ||||
|     if (!document.querySelector('.theme-switch-overlay')) { | ||||
|       const overlay = document.createElement('div'); | ||||
|       overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50'; | ||||
|       overlay.style.opacity = '0'; | ||||
|       overlay.style.transition = 'opacity 0.3s ease-out'; | ||||
|       document.body.appendChild(overlay); | ||||
|     } | ||||
|  | ||||
|     // Toggle theme when any theme toggle button is clicked | ||||
|     themeToggles.forEach((toggle) => { | ||||
|       // Add event listeners for both click and touch events | ||||
|       ['click', 'touchend'].forEach((eventType) => { | ||||
|         toggle.addEventListener( | ||||
|           eventType, | ||||
|           (e) => { | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|  | ||||
|             // Get click/touch position for radial animation | ||||
|             let x, y; | ||||
|             if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) { | ||||
|               const rect = toggle.getBoundingClientRect(); | ||||
|               x = e.changedTouches[0].clientX - rect.left; | ||||
|               y = e.changedTouches[0].clientY - rect.top; | ||||
|             } else { | ||||
|               const rect = toggle.getBoundingClientRect(); | ||||
|               x = e.clientX - rect.left; | ||||
|               y = e.clientY - rect.top; | ||||
|             } | ||||
|  | ||||
|             // Set the position variables for the radial gradient | ||||
|             document.documentElement.style.setProperty('--x', `${x}px`); | ||||
|             document.documentElement.style.setProperty('--y', `${y}px`); | ||||
|  | ||||
|             // Get the overlay element | ||||
|             const overlay = document.querySelector('.theme-switch-overlay'); | ||||
|  | ||||
|             // Determine the new theme | ||||
|             const isDark = document.documentElement.classList.contains('dark'); | ||||
|             const newTheme = isDark ? 'light' : 'dark'; | ||||
|  | ||||
|             // Show overlay during transition | ||||
|             if (overlay) { | ||||
|               overlay.style.backgroundColor = | ||||
|                 newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)'; | ||||
|               overlay.style.opacity = '1'; | ||||
|             } | ||||
|  | ||||
|             // Add transition class | ||||
|             document.documentElement.classList.add('theme-switching'); | ||||
|  | ||||
|             // Force a reflow to ensure all elements update | ||||
|             document.body.offsetHeight; | ||||
|  | ||||
|             // Toggle dark mode with a slight delay to allow overlay to appear | ||||
|             setTimeout(() => { | ||||
|               if (isDark) { | ||||
|                 document.documentElement.classList.remove('dark'); | ||||
|               } else { | ||||
|                 document.documentElement.classList.add('dark'); | ||||
|               } | ||||
|  | ||||
|               // Store the preference | ||||
|               localStorage.setItem('theme', newTheme); | ||||
|  | ||||
|               // Dispatch a custom event for other components to react to | ||||
|               document.dispatchEvent( | ||||
|                 new CustomEvent('themeChanged', { | ||||
|                   detail: { isDark: newTheme === 'dark' }, | ||||
|                 }) | ||||
|               ); | ||||
|  | ||||
|               // Force another reflow to ensure all elements update | ||||
|               document.body.offsetHeight; | ||||
|  | ||||
|               // Hide overlay after theme has changed | ||||
|               setTimeout(() => { | ||||
|                 if (overlay) { | ||||
|                   overlay.style.opacity = '0'; | ||||
|                 } | ||||
|  | ||||
|                 // Remove transition class after animation completes | ||||
|                 document.documentElement.classList.remove('theme-switching'); | ||||
|               }, 300); | ||||
|             }, 50); | ||||
|           }, | ||||
|           { passive: false } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       // Add touch feedback | ||||
|       toggle.addEventListener( | ||||
|         'touchstart', | ||||
|         () => { | ||||
|           toggle.classList.add('active-touch'); | ||||
|         }, | ||||
|         { passive: true } | ||||
|       ); | ||||
|  | ||||
|       toggle.addEventListener( | ||||
|         'touchend', | ||||
|         () => { | ||||
|           setTimeout(() => { | ||||
|             toggle.classList.remove('active-touch'); | ||||
|           }, 150); | ||||
|         }, | ||||
|         { passive: true } | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Run setup on load | ||||
|   document.addEventListener('astro:page-load', setupThemeToggle); | ||||
|  | ||||
|   // Also run on page visibility change to ensure theme is consistent | ||||
|   document.addEventListener('visibilitychange', () => { | ||||
|     if (document.visibilityState === 'visible') { | ||||
|       const currentTheme = localStorage.getItem('theme'); | ||||
|       if (currentTheme === 'dark') { | ||||
|         document.documentElement.classList.add('dark'); | ||||
|       } else if (currentTheme === 'light') { | ||||
|         document.documentElement.classList.remove('dark'); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // Listen for system preference changes | ||||
|   window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => { | ||||
|     if (!localStorage.getItem('theme')) { | ||||
|       if (matches) { | ||||
|         document.documentElement.classList.add('dark'); | ||||
|       } else { | ||||
|         document.documentElement.classList.remove('dark'); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Smooth transition for the entire page when theme changes */ | ||||
|   :global(body) { | ||||
|     transition: | ||||
|       background-color 0.5s ease, | ||||
|       color 0.5s ease; | ||||
|   } | ||||
|  | ||||
|   /* Theme transition overlay */ | ||||
|   :global(.theme-switch-overlay) { | ||||
|     position: fixed; | ||||
|     inset: 0; | ||||
|     z-index: 9999; | ||||
|     pointer-events: none; | ||||
|     transition: opacity 0.3s ease-out; | ||||
|   } | ||||
|  | ||||
|   /* Ensure theme transitions apply to all elements */ | ||||
|   :global(.theme-switching *) { | ||||
|     transition-duration: 0.5s !important; | ||||
|     transition-property: background-color, border-color, color, fill, stroke !important; | ||||
|   } | ||||
|  | ||||
|   /* Subtle hover animation */ | ||||
|   #theme-toggle { | ||||
|     transform: translateY(0); | ||||
|     box-shadow: 0 0 0 rgba(0, 0, 0, 0); | ||||
|     -webkit-tap-highlight-color: transparent; /* Remove default mobile tap highlight */ | ||||
|     min-height: 32px; /* Ensure minimum touch target size */ | ||||
|     min-width: 32px; /* Ensure minimum touch target size */ | ||||
|   } | ||||
|  | ||||
|   /* Only apply hover effects on non-touch devices */ | ||||
|   @media (hover: hover) { | ||||
|     #theme-toggle:hover { | ||||
|       transform: translateY(-2px); | ||||
|       box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | ||||
|     } | ||||
|  | ||||
|     #theme-toggle:hover .icon-light:not(.dark .icon-light) { | ||||
|       filter: drop-shadow-sm(0 0 2px rgba(251, 191, 36, 0.6)); | ||||
|       transform: scale(1.1) rotate(15deg); | ||||
|     } | ||||
|  | ||||
|     #theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) { | ||||
|       filter: drop-shadow-sm(0 0 2px rgba(129, 140, 248, 0.6)); | ||||
|       transform: scale(1.1) rotate(-15deg); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Touch feedback */ | ||||
|   #theme-toggle.active-touch { | ||||
|     transform: scale(0.95); | ||||
|     transition: transform 0.15s ease-in-out; | ||||
|   } | ||||
|  | ||||
|   /* Optimize animations for mobile */ | ||||
|   @media (prefers-reduced-motion: reduce) { | ||||
|     .icon-light, | ||||
|     .icon-dark { | ||||
|       transition: all 0.2s ease-out !important; | ||||
|     } | ||||
|  | ||||
|     #theme-toggle, | ||||
|     #theme-toggle:hover { | ||||
|       transform: none; | ||||
|       transition: none; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Adjust size for very small screens */ | ||||
|   @media (max-width: 320px) { | ||||
|     #theme-toggle { | ||||
|       padding: 0.25rem !important; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
		Reference in New Issue
	
	Block a user