feat: visual polish & UX — fade-in, theme toggle, scroll-to-top, footer, cursor, badge

- Add FadeIn component (IntersectionObserver scroll-reveal + load animation)
- Add dark/light theme toggle (useSyncExternalStore + localStorage)
- Add scroll-to-top button (appears after 300px scroll)
- Add configurable footer with social links (gallery.yaml)
- Add crosshair cursor on photo grid items
- Add 'X photos' count badge on subpage cover images
- Add light theme color palette in tokens.css
This commit is contained in:
ralksta
2026-02-14 11:47:26 +01:00
parent e26f091595
commit 52fa16c1c8
57 changed files with 4790 additions and 1984 deletions

View File

@@ -4,42 +4,42 @@
*/
interface CacheEntry<T> {
data: T;
expiresAt: number;
data: T;
expiresAt: number;
}
class MemoryCache {
private store = new Map<string, CacheEntry<unknown>>();
private maxEntries = 200;
private store = new Map<string, CacheEntry<unknown>>();
private maxEntries = 200;
get<T>(key: string): T | null {
const entry = this.store.get(key);
if (!entry) return null;
get<T>(key: string): T | null {
const entry = this.store.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.store.delete(key);
return null;
}
return entry.data as T;
if (Date.now() > entry.expiresAt) {
this.store.delete(key);
return null;
}
set<T>(key: string, data: T, ttlMs: number): void {
// Evict oldest entries if at capacity
if (this.store.size >= this.maxEntries) {
const firstKey = this.store.keys().next().value;
if (firstKey) this.store.delete(firstKey);
}
return entry.data as T;
}
this.store.set(key, {
data,
expiresAt: Date.now() + ttlMs,
});
set<T>(key: string, data: T, ttlMs: number): void {
// Evict oldest entries if at capacity
if (this.store.size >= this.maxEntries) {
const firstKey = this.store.keys().next().value;
if (firstKey) this.store.delete(firstKey);
}
clear(): void {
this.store.clear();
}
this.store.set(key, {
data,
expiresAt: Date.now() + ttlMs,
});
}
clear(): void {
this.store.clear();
}
}
export const cache = new MemoryCache();