feat: useTheme 훅 + 테마 토글 버튼 구현
- useTheme: localStorage 기반 다크/라이트 테마 전환 - data-theme 속성으로 CSS 변수 자동 전환 - Topbar에 Light/Dark 토글 버튼 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
2adcbc9a93
커밋
a132c7eaf8
@ -1,5 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useAuth } from "../../shared/auth";
|
||||
import { useTheme } from "../../shared/hooks";
|
||||
import { useAisTargetPolling } from "../../features/aisPolling/useAisTargetPolling";
|
||||
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
||||
import { LEGACY_ALARM_KINDS } from "../../features/legacyDashboard/model/types";
|
||||
@ -58,6 +59,7 @@ function useLegacyIndex(data: LegacyVesselDataset | null) {
|
||||
|
||||
export function DashboardPage() {
|
||||
const { user, logout } = useAuth();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const uid = user?.id ?? null;
|
||||
|
||||
// ── Data fetching ──
|
||||
@ -259,6 +261,8 @@ export function DashboardPage() {
|
||||
onLogoClick={onLogoClick}
|
||||
userName={user?.name}
|
||||
onLogout={logout}
|
||||
theme={theme}
|
||||
onToggleTheme={toggleTheme}
|
||||
/>
|
||||
|
||||
<DashboardSidebar
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export { usePersistedState } from './usePersistedState';
|
||||
export { useTheme } from './useTheme';
|
||||
|
||||
47
apps/web/src/shared/hooks/useTheme.ts
Normal file
47
apps/web/src/shared/hooks/useTheme.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
type Theme = 'dark' | 'light';
|
||||
|
||||
const STORAGE_KEY = 'wing:theme';
|
||||
const DEFAULT_THEME: Theme = 'dark';
|
||||
|
||||
function readTheme(): Theme {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw === 'light' || raw === 'dark') return raw;
|
||||
} catch {
|
||||
// storage unavailable
|
||||
}
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
const t = readTheme();
|
||||
applyTheme(t);
|
||||
return t;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
const setTheme = useCallback((t: Theme) => {
|
||||
setThemeState(t);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, t);
|
||||
} catch {
|
||||
// quota exceeded or unavailable
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
}, [theme, setTheme]);
|
||||
|
||||
return { theme, setTheme, toggleTheme } as const;
|
||||
}
|
||||
@ -11,9 +11,11 @@ type Props = {
|
||||
onLogoClick?: () => void;
|
||||
userName?: string;
|
||||
onLogout?: () => void;
|
||||
theme?: "dark" | "light";
|
||||
onToggleTheme?: () => void;
|
||||
};
|
||||
|
||||
export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout }: Props) {
|
||||
export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout, theme, onToggleTheme }: Props) {
|
||||
const statusColor =
|
||||
pollingStatus === "ready" ? "#22C55E" : pollingStatus === "loading" ? "#F59E0B" : pollingStatus === "error" ? "#EF4444" : "var(--muted)";
|
||||
return (
|
||||
@ -55,6 +57,15 @@ export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStat
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2.5 whitespace-nowrap text-[10px] font-semibold text-wing-accent">{clock}</div>
|
||||
{onToggleTheme && (
|
||||
<button
|
||||
className="ml-1 cursor-pointer rounded border border-wing-border bg-transparent px-1.5 py-0.5 text-[9px] text-wing-muted transition-all duration-150 hover:border-wing-accent hover:text-wing-text"
|
||||
onClick={onToggleTheme}
|
||||
title={theme === "dark" ? "라이트 모드로 전환" : "다크 모드로 전환"}
|
||||
>
|
||||
{theme === "dark" ? "Light" : "Dark"}
|
||||
</button>
|
||||
)}
|
||||
{userName && (
|
||||
<div className="ml-2.5 flex shrink-0 items-center gap-2">
|
||||
<span className="whitespace-nowrap text-[10px] font-medium text-wing-text">{userName}</span>
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user