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