File name
Commit message
Commit date
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>
);
}