Add several options for theme selection
Some checks failed
Build and Deploy / build (push) Failing after 29s
Some checks failed
Build and Deploy / build (push) Failing after 29s
This commit is contained in:
parent
fe566f9e1a
commit
ca9cd27acd
0
public/patterns/light-pattern.svg
Normal file
0
public/patterns/light-pattern.svg
Normal file
96
src/components/ThemeBackground.astro
Normal file
96
src/components/ThemeBackground.astro
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
/**
|
||||
* ThemeBackground component
|
||||
*
|
||||
* Provides subtle theme-specific background patterns that change with the theme.
|
||||
* These patterns add visual interest without distracting from the content.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
---
|
||||
|
||||
<div class="theme-background" aria-hidden="true">
|
||||
<div class="light-pattern"></div>
|
||||
<div class="dark-pattern"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.theme-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Light theme pattern using CSS gradients */
|
||||
.light-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.03;
|
||||
transition: opacity 0.5s ease;
|
||||
background-color: rgba(235, 219, 178, 0.01);
|
||||
background-image:
|
||||
/* Grid pattern */
|
||||
linear-gradient(to right, rgba(60, 56, 54, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(60, 56, 54, 0.1) 1px, transparent 1px),
|
||||
/* Diagonal lines */
|
||||
linear-gradient(45deg, rgba(214, 93, 14, 0.1) 25%, transparent 25%),
|
||||
/* Dots pattern */
|
||||
radial-gradient(rgba(184, 187, 38, 0.2) 2px, transparent 2px);
|
||||
background-size:
|
||||
20px 20px, /* Grid X */
|
||||
20px 20px, /* Grid Y */
|
||||
100px 100px, /* Diagonal lines */
|
||||
40px 40px; /* Dots */
|
||||
background-position:
|
||||
0 0, /* Grid X */
|
||||
0 0, /* Grid Y */
|
||||
0 0, /* Diagonal lines */
|
||||
20px 20px; /* Dots */
|
||||
}
|
||||
|
||||
/* Dark theme pattern using CSS gradients */
|
||||
.dark-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
background-color: rgba(40, 40, 40, 0.01);
|
||||
background-image:
|
||||
/* Grid pattern */
|
||||
linear-gradient(to right, rgba(235, 219, 178, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(235, 219, 178, 0.1) 1px, transparent 1px),
|
||||
/* Diagonal lines */
|
||||
linear-gradient(45deg, rgba(254, 128, 25, 0.1) 25%, transparent 25%),
|
||||
/* Dots pattern */
|
||||
radial-gradient(rgba(184, 187, 38, 0.2) 2px, transparent 2px);
|
||||
background-size:
|
||||
20px 20px, /* Grid X */
|
||||
20px 20px, /* Grid Y */
|
||||
100px 100px, /* Diagonal lines */
|
||||
40px 40px; /* Dots */
|
||||
background-position:
|
||||
0 0, /* Grid X */
|
||||
0 0, /* Grid Y */
|
||||
0 0, /* Diagonal lines */
|
||||
20px 20px; /* Dots */
|
||||
}
|
||||
|
||||
:global(.dark) .light-pattern {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:global(.dark) .dark-pattern {
|
||||
opacity: 0.05;
|
||||
}
|
||||
</style>
|
88
src/components/ThemeScheduler.astro
Normal file
88
src/components/ThemeScheduler.astro
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
/**
|
||||
* ThemeScheduler component
|
||||
*
|
||||
* Provides automatic theme switching based on time of day.
|
||||
* - Day time (7 AM to 7 PM): Light theme
|
||||
* - Night time (7 PM to 7 AM): Dark theme
|
||||
*
|
||||
* This component only runs its logic if the user has selected the "auto" theme mode.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
---
|
||||
|
||||
<script is:inline>
|
||||
// Time-based theme switching
|
||||
(function() {
|
||||
// Only run if user has selected auto mode
|
||||
const themeMode = localStorage.getItem('theme-mode');
|
||||
|
||||
if (themeMode === 'auto') {
|
||||
const checkTime = () => {
|
||||
const hour = new Date().getHours();
|
||||
const isDayTime = hour >= 7 && hour < 19; // 7 AM to 7 PM
|
||||
|
||||
const theme = isDayTime ? 'light' : 'dark';
|
||||
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||
|
||||
if (theme !== currentTheme) {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
// Dispatch theme changed event
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
||||
detail: { theme, automatic: true, mode: 'auto' }
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately
|
||||
checkTime();
|
||||
|
||||
// Check every hour
|
||||
setInterval(checkTime, 60 * 60 * 1000);
|
||||
|
||||
// Also check at specific times (7 AM and 7 PM)
|
||||
const scheduleCheck = () => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours();
|
||||
const minutes = now.getMinutes();
|
||||
|
||||
// Calculate time until next check (either 7 AM or 7 PM)
|
||||
let nextCheckHour;
|
||||
if (hours < 7) {
|
||||
nextCheckHour = 7; // Next check at 7 AM
|
||||
} else if (hours < 19) {
|
||||
nextCheckHour = 19; // Next check at 7 PM
|
||||
} else {
|
||||
nextCheckHour = 7; // Next check at 7 AM tomorrow
|
||||
}
|
||||
|
||||
// Calculate milliseconds until next check
|
||||
let msUntilNextCheck;
|
||||
if (hours >= 19) {
|
||||
// After 7 PM, next check is 7 AM tomorrow
|
||||
msUntilNextCheck = ((24 - hours + 7) * 60 - minutes) * 60 * 1000;
|
||||
} else {
|
||||
// Before 7 PM, next check is either 7 AM or 7 PM today
|
||||
msUntilNextCheck = ((nextCheckHour - hours) * 60 - minutes) * 60 * 1000;
|
||||
}
|
||||
|
||||
// Schedule the next check
|
||||
setTimeout(() => {
|
||||
checkTime();
|
||||
scheduleCheck(); // Schedule the next check after this one
|
||||
}, msUntilNextCheck);
|
||||
};
|
||||
|
||||
// Start the scheduling
|
||||
scheduleCheck();
|
||||
}
|
||||
})();
|
||||
</script>
|
@ -1,46 +1,284 @@
|
||||
---
|
||||
/**
|
||||
* Enhanced ThemeToggle component
|
||||
*
|
||||
* Provides a theme toggle button with a dropdown menu for additional theme options:
|
||||
* - Light Mode
|
||||
* - Dark Mode
|
||||
* - System Preference
|
||||
* - Time-Based (automatic switching based on time of day)
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<button class="border-none bg-none focus:outline-2 focus:outline-offset-2 focus:outline-zag-dark dark:focus:outline-zag-light" id="themeToggle" aria-label="Theme Toggle">
|
||||
<svg
|
||||
width="30px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
name="Theme toggle"
|
||||
<div class="theme-toggle-container" x-data="{ open: false }">
|
||||
<button
|
||||
class="theme-toggle-button zag-interactive"
|
||||
id="themeToggle"
|
||||
aria-label="Theme Toggle"
|
||||
@click="open = !open"
|
||||
>
|
||||
<path
|
||||
class="zag-transition fill-neutral-900 dark:fill-transparent"
|
||||
fill-rule="evenodd"
|
||||
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
|
||||
></path>
|
||||
<path
|
||||
class="zag-transition fill-transparent dark:fill-neutral-100"
|
||||
fill-rule="evenodd"
|
||||
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Sun icon for light mode -->
|
||||
<svg
|
||||
class="theme-icon sun-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
class="zag-transition fill-neutral-900 dark:fill-transparent"
|
||||
fill-rule="evenodd"
|
||||
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<script is:inline>
|
||||
// Theme toggle functionality - works with the flash prevention script in Layout
|
||||
const handleToggleClick = () => {
|
||||
const element = document.documentElement;
|
||||
element.classList.toggle("dark");
|
||||
<!-- Moon icon for dark mode -->
|
||||
<svg
|
||||
class="theme-icon moon-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
class="zag-transition fill-transparent dark:fill-neutral-100"
|
||||
fill-rule="evenodd"
|
||||
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
const isDark = element.classList.contains("dark");
|
||||
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
x-show="open"
|
||||
@click.away="open = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="theme-dropdown"
|
||||
x-cloak
|
||||
>
|
||||
<div class="theme-dropdown-content">
|
||||
<button class="theme-option" id="lightTheme">
|
||||
<svg class="theme-option-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<!-- Sun icon -->
|
||||
<circle cx="12" cy="12" r="5" fill="currentColor"></circle>
|
||||
<path d="M12 3V1M12 23v-2M3 12H1m22 0h-2M5.6 5.6L4.2 4.2m14.6 14.6l-1.4-1.4M5.6 18.4l-1.4 1.4M18.4 5.6l1.4-1.4" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
<span>Light Mode</span>
|
||||
</button>
|
||||
|
||||
// Dispatch a custom event that other components can listen for
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
||||
detail: { theme: isDark ? 'dark' : 'light' }
|
||||
}));
|
||||
};
|
||||
<button class="theme-option" id="darkTheme">
|
||||
<svg class="theme-option-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<!-- Moon icon -->
|
||||
<path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z" fill="currentColor"></path>
|
||||
</svg>
|
||||
<span>Dark Mode</span>
|
||||
</button>
|
||||
|
||||
// Add event listener when the DOM is ready
|
||||
<button class="theme-option" id="systemTheme">
|
||||
<svg class="theme-option-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<!-- System icon -->
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" stroke="currentColor" stroke-width="2" fill="none"></rect>
|
||||
<path d="M8 21h8M12 17v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
<span>System Preference</span>
|
||||
</button>
|
||||
|
||||
<button class="theme-option" id="autoTheme">
|
||||
<svg class="theme-option-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<!-- Clock icon -->
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"></circle>
|
||||
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
<span>Time-Based</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.theme-toggle-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-toggle-button {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle-button:hover {
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
border-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.theme-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
background-color: var(--color-zag-light);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
width: 200px;
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.dark) .theme-dropdown {
|
||||
background-color: var(--color-zag-dark);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.theme-dropdown-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
color: var(--color-zag-dark);
|
||||
}
|
||||
|
||||
:global(.dark) .theme-option {
|
||||
color: var(--color-zag-light);
|
||||
}
|
||||
|
||||
.theme-option:hover {
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
|
||||
.theme-option-icon {
|
||||
margin-right: 12px;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Enhanced theme toggle functionality
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document
|
||||
.getElementById("themeToggle")
|
||||
?.addEventListener("click", handleToggleClick);
|
||||
const lightThemeBtn = document.getElementById('lightTheme');
|
||||
const darkThemeBtn = document.getElementById('darkTheme');
|
||||
const systemThemeBtn = document.getElementById('systemTheme');
|
||||
const autoThemeBtn = document.getElementById('autoTheme');
|
||||
|
||||
// Apply light theme
|
||||
lightThemeBtn?.addEventListener('click', () => {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
localStorage.setItem('theme-preference-explicit', 'true');
|
||||
localStorage.setItem('theme-mode', 'manual');
|
||||
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
||||
detail: { theme: 'light', mode: 'manual' }
|
||||
}));
|
||||
});
|
||||
|
||||
// Apply dark theme
|
||||
darkThemeBtn?.addEventListener('click', () => {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
localStorage.setItem('theme-preference-explicit', 'true');
|
||||
localStorage.setItem('theme-mode', 'manual');
|
||||
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
||||
detail: { theme: 'dark', mode: 'manual' }
|
||||
}));
|
||||
});
|
||||
|
||||
// Use system preference
|
||||
systemThemeBtn?.addEventListener('click', () => {
|
||||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
localStorage.setItem('theme-preference-explicit', 'false');
|
||||
localStorage.setItem('theme-mode', 'system');
|
||||
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
||||
detail: { theme: isDark ? 'dark' : 'light', mode: 'system' }
|
||||
}));
|
||||
});
|
||||
|
||||
// Use time-based theme
|
||||
autoThemeBtn?.addEventListener('click', () => {
|
||||
const hour = new Date().getHours();
|
||||
const isDayTime = hour >= 7 && hour < 19; // 7 AM to 7 PM
|
||||
|
||||
if (!isDayTime) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', isDayTime ? 'light' : 'dark');
|
||||
localStorage.setItem('theme-preference-explicit', 'false');
|
||||
localStorage.setItem('theme-mode', 'auto');
|
||||
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
||||
detail: { theme: isDayTime ? 'light' : 'dark', mode: 'auto' }
|
||||
}));
|
||||
});
|
||||
|
||||
// Listen for system preference changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (localStorage.getItem('theme-mode') === 'system') {
|
||||
const isDark = e.matches;
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
||||
detail: { theme: isDark ? 'dark' : 'light', mode: 'system' }
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Simple toggle functionality for the main button
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
themeToggle?.addEventListener('click', () => {
|
||||
// This is now handled by Alpine.js's x-data and @click
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
94
src/components/ThemeTransitionEffect.astro
Normal file
94
src/components/ThemeTransitionEffect.astro
Normal file
@ -0,0 +1,94 @@
|
||||
---
|
||||
/**
|
||||
* ThemeTransitionEffect component
|
||||
*
|
||||
* Provides a visual transition effect when switching between light and dark themes.
|
||||
* Creates a radial gradient that expands from the theme toggle button.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
---
|
||||
|
||||
<div id="themeTransitionOverlay" class="theme-transition-overlay" aria-hidden="true"></div>
|
||||
|
||||
<style>
|
||||
.theme-transition-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.theme-transition-overlay.light-to-dark {
|
||||
background: radial-gradient(circle at var(--x) var(--y), rgba(40, 40, 40, 0.8) 0%, rgba(40, 40, 40, 0) 50%);
|
||||
}
|
||||
|
||||
.theme-transition-overlay.dark-to-light {
|
||||
background: radial-gradient(circle at var(--x) var(--y), rgba(235, 219, 178, 0.8) 0%, rgba(235, 219, 178, 0) 50%);
|
||||
}
|
||||
|
||||
.theme-transition-overlay.active {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const overlay = document.getElementById('themeTransitionOverlay');
|
||||
|
||||
if (!overlay) return;
|
||||
|
||||
// Track current theme
|
||||
let currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||
|
||||
// Listen for theme changes
|
||||
window.addEventListener('theme-changed', (e: Event) => {
|
||||
const customEvent = e as CustomEvent<{theme: string}>;
|
||||
const newTheme = customEvent.detail.theme;
|
||||
|
||||
if (newTheme === currentTheme) return;
|
||||
|
||||
// Get toggle button position for centered effect
|
||||
const toggleBtn = document.getElementById('themeToggle');
|
||||
let x = '50%';
|
||||
let y = '50%';
|
||||
|
||||
if (toggleBtn) {
|
||||
const rect = toggleBtn.getBoundingClientRect();
|
||||
x = `${rect.left + rect.width / 2}px`;
|
||||
y = `${rect.top + rect.height / 2}px`;
|
||||
}
|
||||
|
||||
// Set the radial gradient position
|
||||
overlay.style.setProperty('--x', x);
|
||||
overlay.style.setProperty('--y', y);
|
||||
|
||||
// Add the appropriate class
|
||||
if (newTheme === 'dark') {
|
||||
overlay.classList.add('light-to-dark');
|
||||
overlay.classList.remove('dark-to-light');
|
||||
} else {
|
||||
overlay.classList.add('dark-to-light');
|
||||
overlay.classList.remove('light-to-dark');
|
||||
}
|
||||
|
||||
// Trigger the animation
|
||||
overlay.classList.add('active');
|
||||
|
||||
// Remove the animation after it completes
|
||||
setTimeout(() => {
|
||||
overlay.classList.remove('active');
|
||||
overlay.classList.remove('light-to-dark');
|
||||
overlay.classList.remove('dark-to-light');
|
||||
}, 500);
|
||||
|
||||
// Update current theme
|
||||
currentTheme = newTheme;
|
||||
});
|
||||
});
|
||||
</script>
|
@ -3,6 +3,9 @@ import Footer from "../components/Footer.astro";
|
||||
import Header from "../components/Header.astro";
|
||||
import SearchScript from "../components/SearchScript.astro";
|
||||
import LoadingOverlay from "../components/common/LoadingOverlay.astro";
|
||||
import ThemeTransitionEffect from "../components/ThemeTransitionEffect.astro";
|
||||
import ThemeBackground from "../components/ThemeBackground.astro";
|
||||
import ThemeScheduler from "../components/ThemeScheduler.astro";
|
||||
import "../styles/global.css";
|
||||
---
|
||||
|
||||
@ -116,6 +119,12 @@ import "../styles/global.css";
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Time-based theme scheduler -->
|
||||
<ThemeScheduler />
|
||||
|
||||
<!-- Theme transition script -->
|
||||
<script src="/src/scripts/ThemeTransition.js"></script>
|
||||
|
||||
<SearchScript />
|
||||
<slot name="head" />
|
||||
</head>
|
||||
@ -123,6 +132,10 @@ import "../styles/global.css";
|
||||
<!-- Loading overlay for long loading times -->
|
||||
<LoadingOverlay />
|
||||
|
||||
<!-- Theme-specific background patterns and transition effect -->
|
||||
<ThemeBackground />
|
||||
<ThemeTransitionEffect />
|
||||
|
||||
<Header />
|
||||
<main>
|
||||
<slot />
|
||||
|
116
src/scripts/ThemeTransition.js
Normal file
116
src/scripts/ThemeTransition.js
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Theme Transition Script
|
||||
*
|
||||
* Handles element-specific animations during theme changes.
|
||||
* Applies staggered animations to different elements when the theme changes.
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Listen for theme change events
|
||||
window.addEventListener('theme-changed', (e) => {
|
||||
const customEvent = e instanceof CustomEvent ? e : null;
|
||||
const theme = customEvent?.detail?.theme || (document.documentElement.classList.contains('dark') ? 'dark' : 'light');
|
||||
|
||||
// Skip animations if reduced motion is preferred
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Animate headings with staggered delay
|
||||
document.querySelectorAll('h1, h2, h3').forEach((heading, index) => {
|
||||
// Remove any existing animation classes
|
||||
heading.classList.remove('theme-animate-slide');
|
||||
|
||||
// Force a reflow to restart the animation
|
||||
void heading.offsetWidth;
|
||||
|
||||
// Add animation class with delay based on index
|
||||
setTimeout(() => {
|
||||
heading.classList.add('theme-animate-slide');
|
||||
}, 50 + (index * 30));
|
||||
});
|
||||
|
||||
// Animate cards and sections with staggered delay
|
||||
document.querySelectorAll('.card, article, section:not(section section)').forEach((element, index) => {
|
||||
// Remove any existing animation classes
|
||||
element.classList.remove('theme-animate-scale');
|
||||
|
||||
// Force a reflow to restart the animation
|
||||
void element.offsetWidth;
|
||||
|
||||
// Add animation class with delay based on index
|
||||
setTimeout(() => {
|
||||
element.classList.add('theme-animate-scale');
|
||||
}, 100 + (index * 40));
|
||||
});
|
||||
|
||||
// Animate images and icons
|
||||
document.querySelectorAll('img, svg').forEach((element, index) => {
|
||||
// Remove any existing animation classes
|
||||
element.classList.remove('theme-animate-fade');
|
||||
|
||||
// Force a reflow to restart the animation
|
||||
void element.offsetWidth;
|
||||
|
||||
// Add animation class with delay based on index
|
||||
setTimeout(() => {
|
||||
element.classList.add('theme-animate-fade');
|
||||
}, 150 + (index * 20));
|
||||
});
|
||||
|
||||
// Add theme-specific classes to enhance certain elements
|
||||
if (theme === 'dark') {
|
||||
// Dark theme enhancements
|
||||
document.querySelectorAll('code, pre').forEach(element => {
|
||||
element.classList.add('dark-theme-code');
|
||||
});
|
||||
|
||||
// Add subtle glow to important buttons in dark mode
|
||||
document.querySelectorAll('.button-primary, .cta-button').forEach(element => {
|
||||
element.classList.add('dark-theme-glow');
|
||||
});
|
||||
} else {
|
||||
// Light theme enhancements
|
||||
document.querySelectorAll('code, pre').forEach(element => {
|
||||
element.classList.remove('dark-theme-code');
|
||||
});
|
||||
|
||||
// Remove glow from buttons in light mode
|
||||
document.querySelectorAll('.button-primary, .cta-button').forEach(element => {
|
||||
element.classList.remove('dark-theme-glow');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add CSS classes for theme-specific enhancements
|
||||
const addThemeStyles = () => {
|
||||
// Create a style element
|
||||
const style = document.createElement('style');
|
||||
|
||||
// Add CSS for dark theme code blocks
|
||||
style.textContent = `
|
||||
.dark-theme-code {
|
||||
box-shadow: 0 0 8px rgba(254, 128, 25, 0.3);
|
||||
}
|
||||
|
||||
.dark-theme-glow {
|
||||
box-shadow: 0 0 12px rgba(254, 128, 25, 0.4);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-animate-fade,
|
||||
.theme-animate-slide,
|
||||
.theme-animate-scale {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Append the style element to the head
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
// Add the styles when the DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', addThemeStyles);
|
@ -8,9 +8,21 @@ html:not(.theme-loaded) body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Ensure smooth transitions between themes */
|
||||
/* Enhanced transitions between themes */
|
||||
html.theme-loaded body {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
transition: background-color 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Add transition to all themed elements */
|
||||
.theme-transition-element {
|
||||
transition: background-color 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
fill 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
stroke 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Font loading states */
|
||||
@ -181,16 +193,58 @@ html.fonts-loaded body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.zag-transition {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition:
|
||||
background-color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
fill var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
border-color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
transform var(--zag-transition-duration) var(--zag-transition-timing-function);
|
||||
}
|
||||
.zag-transition {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition:
|
||||
background-color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
fill var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
border-color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
transform var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
opacity var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||
box-shadow var(--zag-transition-duration) var(--zag-transition-timing-function);
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme transition animations for specific elements */
|
||||
@keyframes theme-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes theme-slide-up {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes theme-scale-in {
|
||||
from {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-animate-fade {
|
||||
animation: theme-fade-in 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.theme-animate-slide {
|
||||
animation: theme-slide-up 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.theme-animate-scale {
|
||||
animation: theme-scale-in 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
/* Base backgrounds and text */
|
||||
.zag-bg {
|
||||
|
Loading…
x
Reference in New Issue
Block a user