0f083d8e5d
Build & Deploy / Build & Push Docker Image (push) Failing after 49s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Has been cancelled
Test / Admin Unit Tests (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
'use client';
|
|
|
|
import { persistTheme, themePreferenceEvent } from '@/lib/theme/client';
|
|
import { type ThemePreference } from '@/lib/theme/config';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import styles from './ThemePill.module.css';
|
|
|
|
interface Props {
|
|
initialPreference: ThemePreference;
|
|
}
|
|
|
|
const labels: Record<ThemePreference, string> = {
|
|
light: 'Light',
|
|
dark: 'Dark',
|
|
system: 'System',
|
|
};
|
|
|
|
export function ThemePill({ initialPreference }: Props) {
|
|
const [pref, setPref] = useState<ThemePreference>(initialPreference);
|
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setPref(initialPreference);
|
|
}, [initialPreference]);
|
|
|
|
useEffect(() => {
|
|
const handler = () => {
|
|
const stored = localStorage.getItem('hpc.theme.preference');
|
|
if (stored === 'light' || stored === 'dark' || stored === 'system') setPref(stored);
|
|
};
|
|
window.addEventListener(themePreferenceEvent, handler);
|
|
return () => window.removeEventListener(themePreferenceEvent, handler);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
function handleClick(e: MouseEvent) {
|
|
if (!menuRef.current?.contains(e.target as Node)) setOpen(false);
|
|
}
|
|
document.addEventListener('mousedown', handleClick);
|
|
return () => document.removeEventListener('mousedown', handleClick);
|
|
}, []);
|
|
|
|
const selectTheme = (next: ThemePreference) => {
|
|
setOpen(false);
|
|
persistTheme(next);
|
|
setPref(next);
|
|
};
|
|
|
|
const options: ThemePreference[] = ['light', 'dark', 'system'];
|
|
const otherOptions = options.filter((o) => o !== pref);
|
|
|
|
const displayLabel = pref === 'dark' ? 'Dark' : 'Light';
|
|
|
|
return (
|
|
<div ref={menuRef} className={styles.wrap}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((v) => !v)}
|
|
className={styles.button}
|
|
aria-expanded={open}
|
|
aria-haspopup="menu"
|
|
aria-label={displayLabel}
|
|
>
|
|
<span className={styles.label}>{displayLabel}</span>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth={2}
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
{open && (
|
|
<div className={styles.menu}>
|
|
{otherOptions.map((o) => (
|
|
<button
|
|
key={o}
|
|
type="button"
|
|
onClick={() => selectTheme(o)}
|
|
className={styles.option}
|
|
>
|
|
<span>{labels[o]}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|