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
|
||||||
|
*/
|
||||||
|
---
|
||||||
|
|
||||||
---
|
<div class="theme-toggle-container" x-data="{ open: false }">
|
||||||
|
<button
|
||||||
<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">
|
class="theme-toggle-button zag-interactive"
|
||||||
<svg
|
id="themeToggle"
|
||||||
width="30px"
|
aria-label="Theme Toggle"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@click="open = !open"
|
||||||
viewBox="0 0 24 24"
|
|
||||||
name="Theme toggle"
|
|
||||||
>
|
>
|
||||||
<path
|
<!-- Sun icon for light mode -->
|
||||||
class="zag-transition fill-neutral-900 dark:fill-transparent"
|
<svg
|
||||||
fill-rule="evenodd"
|
class="theme-icon sun-icon"
|
||||||
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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
></path>
|
width="24"
|
||||||
<path
|
height="24"
|
||||||
class="zag-transition fill-transparent dark:fill-neutral-100"
|
viewBox="0 0 24 24"
|
||||||
fill-rule="evenodd"
|
aria-hidden="true"
|
||||||
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>
|
<path
|
||||||
</svg>
|
class="zag-transition fill-neutral-900 dark:fill-transparent"
|
||||||
</button>
|
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>
|
<!-- Moon icon for dark mode -->
|
||||||
// Theme toggle functionality - works with the flash prevention script in Layout
|
<svg
|
||||||
const handleToggleClick = () => {
|
class="theme-icon moon-icon"
|
||||||
const element = document.documentElement;
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
element.classList.toggle("dark");
|
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");
|
<!-- Dropdown menu -->
|
||||||
localStorage.setItem("theme", isDark ? "dark" : "light");
|
<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
|
<button class="theme-option" id="darkTheme">
|
||||||
window.dispatchEvent(new CustomEvent('theme-changed', {
|
<svg class="theme-option-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||||
detail: { theme: isDark ? 'dark' : 'light' }
|
<!-- 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.addEventListener('DOMContentLoaded', () => {
|
||||||
document
|
const lightThemeBtn = document.getElementById('lightTheme');
|
||||||
.getElementById("themeToggle")
|
const darkThemeBtn = document.getElementById('darkTheme');
|
||||||
?.addEventListener("click", handleToggleClick);
|
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>
|
</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 Header from "../components/Header.astro";
|
||||||
import SearchScript from "../components/SearchScript.astro";
|
import SearchScript from "../components/SearchScript.astro";
|
||||||
import LoadingOverlay from "../components/common/LoadingOverlay.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";
|
import "../styles/global.css";
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -116,6 +119,12 @@ import "../styles/global.css";
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Time-based theme scheduler -->
|
||||||
|
<ThemeScheduler />
|
||||||
|
|
||||||
|
<!-- Theme transition script -->
|
||||||
|
<script src="/src/scripts/ThemeTransition.js"></script>
|
||||||
|
|
||||||
<SearchScript />
|
<SearchScript />
|
||||||
<slot name="head" />
|
<slot name="head" />
|
||||||
</head>
|
</head>
|
||||||
@ -123,6 +132,10 @@ import "../styles/global.css";
|
|||||||
<!-- Loading overlay for long loading times -->
|
<!-- Loading overlay for long loading times -->
|
||||||
<LoadingOverlay />
|
<LoadingOverlay />
|
||||||
|
|
||||||
|
<!-- Theme-specific background patterns and transition effect -->
|
||||||
|
<ThemeBackground />
|
||||||
|
<ThemeTransitionEffect />
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
<slot />
|
<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;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure smooth transitions between themes */
|
/* Enhanced transitions between themes */
|
||||||
html.theme-loaded body {
|
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 */
|
/* Font loading states */
|
||||||
@ -181,16 +193,58 @@ html.fonts-loaded body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zag-transition {
|
.zag-transition {
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
transition:
|
transition:
|
||||||
background-color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
background-color var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||||
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),
|
fill var(--zag-transition-duration) var(--zag-transition-timing-function),
|
||||||
border-color 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);
|
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 */
|
/* Base backgrounds and text */
|
||||||
.zag-bg {
|
.zag-bg {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user