"use client"; import { useSyncExternalStore } from "react"; import { DEFAULT_THEME_PREFERENCE, THEME_ATTRIBUTE_NAME, THEME_PREFERENCE_CYCLE, THEME_STORAGE_KEY, type ThemePreference, } from "@/lib/constants/theme"; import styles from "../design.module.scss"; const LABELS: Record = { system: "시스템", light: "라이트", dark: "다크", }; /** * `` 자체가 SSOT이므로 React state를 두지 않고 DOM을 직접 구독한다 * (앱 헤더의 테마 토글과 동일한 원칙 — 서버/클라이언트 출력이 갈리지 않는다). * 서버 스냅샷은 항상 기본값(system)이라 첫 렌더가 결정적이다. */ function subscribe(onChange: () => void): () => void { const observer = new MutationObserver(onChange); observer.observe(document.documentElement, { attributes: true, attributeFilter: [THEME_ATTRIBUTE_NAME], }); return () => observer.disconnect(); } function getSnapshot(): ThemePreference { const value = document.documentElement.getAttribute(THEME_ATTRIBUTE_NAME); return value === "light" || value === "dark" ? value : DEFAULT_THEME_PREFERENCE; } function getServerSnapshot(): ThemePreference { return DEFAULT_THEME_PREFERENCE; } function applyTheme(next: ThemePreference): void { const root = document.documentElement; // 전환 트랜지션은 200ms 동안만 켜지는 opt-in이다(@fox/styles/_root.scss). root.setAttribute("data-theme-transition", ""); if (next === "system") { root.removeAttribute(THEME_ATTRIBUTE_NAME); localStorage.removeItem(THEME_STORAGE_KEY); } else { root.setAttribute(THEME_ATTRIBUTE_NAME, next); localStorage.setItem(THEME_STORAGE_KEY, next); } window.setTimeout(() => { root.removeAttribute("data-theme-transition"); }, 200); } /** 카탈로그에서 라이트/다크 토큰을 눈으로 대조하기 위한 개발용 전환기. */ export function ThemeSwitch() { const current = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); return (
{THEME_PREFERENCE_CYCLE.map((preference) => ( ))}
); }