280 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			280 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
---
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
<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>
 |