Version 1.0.0

This commit is contained in:
2025-05-10 16:52:45 +02:00
commit bed95bff35
459 changed files with 36475 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
import { useCallback, useEffect, useState } from 'react';
export type Appearance = 'light' | 'dark' | 'contrast';
const prefersDark = () => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const setCookie = (name: string, value: string, days = 365) => {
if (typeof document === 'undefined') {
return;
}
const maxAge = days * 24 * 60 * 60;
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
};
const applyTheme = (appearance: Appearance) => {
if (appearance === 'contrast') {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('contrast');
} else if (appearance === 'dark') {
document.documentElement.classList.remove('contrast');
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('contrast');
document.documentElement.classList.remove('dark');
}
};
const mediaQuery = () => {
if (typeof window === 'undefined') {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
};
export function initializeTheme() {
const savedAppearance = localStorage.getItem('appearance') as Appearance;
const prefersDark = mediaQuery()?.matches;
// If no saved appearance and user prefers dark, set dark mode
if (!savedAppearance && prefersDark) {
applyTheme('dark');
localStorage.setItem('appearance', 'dark');
} else {
applyTheme(savedAppearance || 'light');
}
}
export function useAppearance() {
const [appearance, setAppearance] = useState<Appearance>('light');
const updateAppearance = useCallback((mode: Appearance) => {
setAppearance(mode);
// Store in localStorage for client-side persistence...
localStorage.setItem('appearance', mode);
// Store in cookie for SSR...
setCookie('appearance', mode);
applyTheme(mode);
}, []);
useEffect(() => {
const savedAppearance = localStorage.getItem('appearance') as Appearance | null;
const prefersDark = mediaQuery()?.matches;
// If no saved appearance and user prefers dark, set dark mode
if (!savedAppearance && prefersDark) {
updateAppearance('dark');
} else {
updateAppearance(savedAppearance || 'light');
}
}, [updateAppearance]);
return { appearance, updateAppearance } as const;
}

View File

@@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';
type FontSize = 'normal' | 'large' | 'larger';
export function useFontSize() {
const [fontSize, setFontSize] = useState<FontSize>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('font-size');
if (saved && ['normal', 'large', 'larger'].includes(saved)) {
return saved as FontSize;
}
}
return 'normal';
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('text-normal', 'text-large', 'text-larger');
root.classList.add(`text-${fontSize}`);
localStorage.setItem('font-size', fontSize);
}, [fontSize]);
return {
fontSize,
setFontSize,
};
}

View File

@@ -0,0 +1,8 @@
import { useCallback } from 'react';
export function useMobileNavigation() {
return useCallback(() => {
// Remove pointer-events style from body...
document.body.style.removeProperty('pointer-events');
}, []);
}

View File

@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = useState<boolean>();
useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
}