| /* |
| * 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'; |
| }; |