blob: cd39897324ecdd8ef9058a8cc644b3834b944f09 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '@/app/store';
import type { ThemeMode, ResolvedTheme } from '@/theme/tokens';
export const THEME_STORAGE_KEY = 'devlake.theme';
const NEXT_MODE: Record<ThemeMode, ThemeMode> = {
light: 'dark',
dark: 'system',
system: 'light',
};
const isThemeMode = (value: unknown): value is ThemeMode => value === 'light' || value === 'dark' || value === 'system';
const loadInitialMode = (): ThemeMode => {
if (typeof window === 'undefined') return 'system';
try {
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
if (isThemeMode(stored)) return stored;
} catch {
// localStorage may be unavailable (e.g., SSR / privacy mode)
}
return 'system';
};
const persistMode = (mode: ThemeMode): void => {
if (typeof window === 'undefined') return;
try {
window.localStorage.setItem(THEME_STORAGE_KEY, mode);
} catch {
// localStorage may be unavailable (e.g., SSR / privacy mode)
}
};
interface ThemeState {
mode: ThemeMode;
}
const initialState: ThemeState = {
mode: loadInitialMode(),
};
export const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
setMode(state, action: PayloadAction<ThemeMode>) {
state.mode = action.payload;
persistMode(action.payload);
},
cycleMode(state) {
state.mode = NEXT_MODE[state.mode];
persistMode(state.mode);
},
},
});
export const { setMode, cycleMode } = themeSlice.actions;
export default themeSlice.reducer;
export const selectThemeMode = (state: RootState): ThemeMode => state.theme.mode;
export const resolveThemeMode = (mode: ThemeMode): ResolvedTheme => {
if (mode !== 'system') return mode;
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};