File name
Commit message
Commit date
08-14
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { useThemePreference } from '@/app/(protected)/(basic)/_hooks/use-theme-preference';
import { IconButton } from '@/components/ui/icon-button';
import { MonitorIcon } from '@/components/ui/icons/monitor-icon';
import { MoonIcon } from '@/components/ui/icons/moon-icon';
import { SunIcon } from '@/components/ui/icons/sun-icon';
/**
* 헤더 우측 테마 토글 버튼 — 아이콘 3종(모니터/해/달)을 항상 DOM에 전부 렌더하고, design이
* globals.css에 정의한 `theme-system`/`theme-light`/`theme-dark` 커스텀 variant가 `<html
* data-theme>` 값에 따라 CSS만으로 하나만 보이게 한다. 서버·클라이언트 출력이 항상 동일해
* hydration 불일치가 없다 — React state로 아이콘을 고르지 않는다(계획 §4-4, §10 게이트 확정).
*
* 아이콘·라벨 쌍을 감싸는 `<span className="hidden theme-*:contents">` 래퍼에 표시 전환을
* 건다(아이콘 자체나 `sr-only` 라벨 각각에 개별로 걸지 않는다) — `display: contents`는 자신의
* 박스를 없애는 것이라, `sr-only`의 clip 기법이 의존하는 박스 자체가 있는 요소(라벨 span)에
* 직접 걸면 클리핑이 무효화돼 텍스트가 그대로 노출된다. 래퍼는 그 자신의 박스만 지우고
* 자식(아이콘의 내부 span, 라벨 span)은 각자의 박스를 그대로 유지하므로 안전하다. 아이콘
* 컴포넌트에는 `className`을 넘기지 않는다 — svg가 아니라 이 래퍼가 표시를 전담한다.
*/
export function ThemeToggleButton() {
const { cycleThemePreference } = useThemePreference();
return (
<IconButton onClick={cycleThemePreference}>
<span className="hidden theme-system:contents">
<MonitorIcon />
<span className="sr-only">테마 변경 (현재: 시스템 설정)</span>
</span>
<span className="hidden theme-light:contents">
<SunIcon />
<span className="sr-only">테마 변경 (현재: 라이트)</span>
</span>
<span className="hidden theme-dark:contents">
<MoonIcon />
<span className="sr-only">테마 변경 (현재: 다크)</span>
</span>
</IconButton>
);
}