Skip to content

Commit

Permalink
refactor theme and mode in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
nemanjam committed Aug 21, 2024
1 parent 9733bed commit 59ec5cc
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 71 deletions.
3 changes: 3 additions & 0 deletions docs/working-notes/todo3.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,5 +505,8 @@ a href open in new tab
fix links page links color for history back view transition

refactor theme script, osDefaultMode, appDefaultMode, storedMode
change meta theme bg color with js, astro-paper

replace import config with import.meta.env.VAR
------------
```
4 changes: 2 additions & 2 deletions src/components/Giscus.astro
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ const { class: className } = Astro.props;
import 'giscus';

import { SELECTORS } from '@/constants/dom';
import { THEME_CONFIG } from '@/constants/themes';
import { getCurrentMode, sendModeToGiscus } from '@/utils/dom';
import { THEME_CONFIG } from '@/constants/theme';
import { getCurrentMode, sendModeToGiscus } from '@/utils/theme';

import type { ChangeThemeCustomEvent } from '@/types/constants';

Expand Down
56 changes: 34 additions & 22 deletions src/components/ThemeScript.astro
Original file line number Diff line number Diff line change
@@ -1,43 +1,56 @@
---
import * as themeConstants from '@/constants/themes';
import * as themeConstants from '@/constants/theme';
import { getDefaultThemes } from '@/utils/theme';
const defaultThemes = getDefaultThemes();
---

{/* Inlined to avoid flash of white content. */}
<script is:inline define:vars={{ themeConstants }}>
<script is:inline define:vars={{ themeConstants, defaultThemes }}>
// this is JavaScript, not TypeScript
const { MODES, THEMES, THEME_CONFIG } = themeConstants;
const { DATA_ATTRIBUTE, CHANGE_EVENT, LOCAL_STORAGE_KEY } = THEME_CONFIG;

// this is JavaScript, not TypeScript
const defaultThemes = { light: THEMES[0], dark: THEMES[1] };
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)');

const lightModePreference = window.matchMedia('(prefers-color-scheme: light)');
// 1. stored mode
// 2. default app mode, config
// 3. OS mode

// light is default
const getMode = (themeMode) => (themeMode === MODES.dark ? MODES.dark : MODES.light);

const getDefaultTheme = (themeMode) => defaultThemes[getMode(themeMode)];

const changeThemeMode = (themeMode) => {
const storedTheme = getTheme();
const setMode = (themeMode) => {
const storedTheme = getStoredTheme();

if (!storedTheme) return getDefaultTheme(themeMode);

const newTheme = { ...storedTheme, mode: getMode(themeMode) };
return newTheme;
};

const getUserPreference = () => {
const getOSMode = () => (darkModePreference.matches ? MODES.dark : MODES.light);

const getOSTheme = () => {
const themeMode = getOSMode();
const defaultOSTheme = getDefaultTheme(themeMode);

return defaultOSTheme;
};

const getTheme = () => {
// either from storage
const storedTheme = getTheme();
const storedTheme = getStoredTheme();
if (storedTheme) return storedTheme;

// or fallback to browser default
const preferedMode = lightModePreference.matches ? MODES.light : MODES.dark;
const defaultTheme = getDefaultTheme(preferedMode);
return defaultTheme;
// or fallback to default theme for OS mode
const defaultOSTheme = getOSTheme();
return defaultOSTheme;
};

const getTheme = () => {
const getStoredTheme = () => {
const storedThemeString =
typeof localStorage !== 'undefined' && localStorage.getItem(LOCAL_STORAGE_KEY);

Expand All @@ -46,7 +59,7 @@ import * as themeConstants from '@/constants/themes';
let storedTheme;
try {
storedTheme = JSON.parse(storedThemeString);
} catch (error) {
} catch (_error) {
localStorage.removeItem(LOCAL_STORAGE_KEY);
return null;
}
Expand Down Expand Up @@ -100,21 +113,20 @@ import * as themeConstants from '@/constants/themes';
};

// initial setup
setTheme(getUserPreference());
setTheme(getTheme());

// fails in firefox
// View Transitions hook to restore theme
document.addEventListener('astro:after-swap', () => setTheme(getUserPreference()));
document.addEventListener('astro:after-swap', () => setTheme(getTheme()));

// listen for theme-change custom event, fired in src/components/ThemeToggle.astro
document.addEventListener(CHANGE_EVENT, (event) => {
setTheme(event.detail.theme);
});

// listen for prefers-color-scheme change.
lightModePreference.addEventListener('change', (event) => {
const newMode = event.matches ? MODES.light : MODES.dark;
const newTheme = changeThemeMode(newMode);
// listen for prefers-color-scheme change
darkModePreference.addEventListener('change', (event) => {
const newMode = event.matches ? MODES.dark : MODES.light;
const newTheme = setMode(newMode);
setTheme(newTheme);
});
</script>
4 changes: 2 additions & 2 deletions src/components/ThemeToggle.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { Icon } from 'astro-icon/components';
</theme-toggle>

<script>
import { THEME_CONFIG } from '@/constants/themes';
import { getNextTheme } from '@/utils/dom';
import { THEME_CONFIG } from '@/constants/theme';
import { getNextTheme } from '@/utils/theme';

import type { ChangeThemeCustomEvent } from '@/types/constants';

Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const configData: ConfigType = {
PAGE_SIZE_POST_CARD: 3,
PAGE_SIZE_POST_CARD_SMALL: 6,
MORE_POSTS_COUNT: 3,
DEFAULT_MODE: 'light',
DEFAULT_THEME: 'default-light',
AUTHOR_NAME: 'Nemanja Mitic',
AUTHOR_EMAIL: '[email protected]',
AUTHOR_GITHUB: 'https://github.com/nemanjam',
Expand Down
12 changes: 4 additions & 8 deletions src/constants/themes.ts → src/constants/theme.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
export const MODES = {
dark: 'dark',
light: 'light',
// add auto
} as const;

export const DEFAULT_THEMES = {
light: {
export const THEMES = [
{
mode: MODES.light,
name: 'default-light',
},
dark: {
{
mode: MODES.dark,
name: 'default-dark',
},
} as const;

export const THEMES = [
DEFAULT_THEMES.light,
DEFAULT_THEMES.dark,
{
mode: MODES.light,
name: 'green-light',
Expand Down
4 changes: 2 additions & 2 deletions src/pages/links.astro
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ const { GITHUB_MARKDOWN_BODY_ID } = SELECTORS;

<script>
import { SELECTORS } from '@/constants/dom';
import { MODES, THEME_CONFIG } from '@/constants/themes';
import { getCurrentMode } from '@/utils/dom';
import { MODES, THEME_CONFIG } from '@/constants/theme';
import { getCurrentMode } from '@/utils/theme';

import type { ChangeThemeCustomEvent, Mode } from '@/types/constants';

Expand Down
5 changes: 5 additions & 0 deletions src/schemas/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { z } from 'zod';
export const nodeEnvValues = ['development', 'test', 'production'] as const;
export const booleanValues = ['true', 'false', ''] as const;

export const modeValues = ['light', 'dark'] as const;
export const themeValues = ['default-light', 'default-dark', 'green-light', 'green-dark'] as const;

export const configSchema = z.object({
NODE_ENV: z.enum(nodeEnvValues),
PREVIEW_MODE: z
Expand All @@ -16,6 +19,8 @@ export const configSchema = z.object({
PAGE_SIZE_POST_CARD: z.number(),
PAGE_SIZE_POST_CARD_SMALL: z.number(),
MORE_POSTS_COUNT: z.number(),
DEFAULT_MODE: z.enum(modeValues), // check that theme and mode match
DEFAULT_THEME: z.enum(themeValues),
AUTHOR_NAME: z.string().min(1),
AUTHOR_EMAIL: z.string().email(),
AUTHOR_GITHUB: z.string().url(),
Expand Down
2 changes: 1 addition & 1 deletion src/types/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CATEGORIES } from '@/constants/collections';
import type { PAGE_METADATA } from '@/constants/metadata';
import type { NAVIGATION_ITEMS } from '@/constants/navigation';
import type { MODES, THEMES } from '@/constants/themes';
import type { MODES, THEMES } from '@/constants/theme';
import type { ValueUnion } from '@/types/utils';
import type { LocalImageProps } from 'astro:assets';

Expand Down
35 changes: 1 addition & 34 deletions src/utils/dom.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,6 @@
import { SELECTORS } from '@/constants/dom';
import { DEFAULT_THEMES, MODES, THEME_CONFIG, THEMES } from '@/constants/themes';

import type { Mode, Theme } from '@/types/constants';

const { MODE_CLASS, DATA_ATTRIBUTE } = THEME_CONFIG;

export const getCurrentMode = () =>
document.documentElement.classList.contains(MODE_CLASS) ? MODES.dark : MODES.light;

export const getCurrentTheme = () => {
const themeName = document.documentElement.getAttribute(DATA_ATTRIBUTE);
const isValidThemeName =
Boolean(themeName) && THEMES.map((theme) => theme.name).includes(themeName as Theme['name']);

if (!isValidThemeName) return null;

const currentTheme = THEMES.find((theme) => theme.name === themeName) as Theme;
return currentTheme;
};

export const getNextTheme = () => {
const currentTheme = getCurrentTheme();

const currentIndex = THEMES.findIndex(
(theme) => currentTheme && currentTheme.name === theme.name
);

if (currentIndex === -1) {
const currentMode = getCurrentMode();
return DEFAULT_THEMES[currentMode];
}

const nextIndex = (currentIndex + 1) % THEMES.length;
return THEMES[nextIndex];
};
import type { Mode } from '@/types/constants';

/*-------------------------------- giscus dark/light mode ------------------------------*/

Expand Down
85 changes: 85 additions & 0 deletions src/utils/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { MODES, THEME_CONFIG, THEMES } from '@/constants/theme';
import { CONFIG } from '@/config';

import type { Mode, Theme } from '@/types/constants';

const { DEFAULT_THEME, DEFAULT_MODE } = CONFIG;
const { MODE_CLASS, DATA_ATTRIBUTE } = THEME_CONFIG;

export const getCurrentMode = () =>
document.documentElement.classList.contains(MODE_CLASS) ? MODES.dark : MODES.light;

export const getCurrentTheme = () => {
const themeName = document.documentElement.getAttribute(DATA_ATTRIBUTE);
const isValidThemeName =
Boolean(themeName) && THEMES.map((theme) => theme.name).includes(themeName as Theme['name']);

if (!isValidThemeName) return null;

const currentTheme = THEMES.find((theme) => theme.name === themeName) as Theme;
return currentTheme;
};

export const getNextTheme = () => {
const currentTheme = getCurrentTheme();

const currentIndex = THEMES.findIndex(
(theme) => currentTheme && currentTheme.name === theme.name
);

if (currentIndex === -1) {
const currentMode = getCurrentMode();
const defaultThemes = getDefaultThemes();

return defaultThemes[currentMode];
}

const nextIndex = (currentIndex + 1) % THEMES.length;
return THEMES[nextIndex];
};

export const validateMode = (mode: Mode): void => {
if (![MODES.light, MODES.dark].includes(mode)) throw new Error(`Invalid mode: ${mode}`);
};

export const validateTheme = (theme: Theme['name']): void => {
if (!THEMES.map((theme) => theme.name).includes(theme))
throw new Error(`Invalid theme: ${theme}`);
};

export const getDefaultThemes = () => {
validateMode(DEFAULT_MODE);
validateTheme(DEFAULT_THEME);

const isDarkMode = DEFAULT_MODE === MODES.dark;

const otherMode = isDarkMode ? MODES.light : MODES.dark;
const otherTheme = DEFAULT_THEME.replace(DEFAULT_MODE, otherMode) as Theme['name'];

validateMode(otherMode);
validateTheme(otherTheme);

const defaultThemes = isDarkMode
? {
light: {
mode: otherMode,
name: otherTheme,
},
dark: {
mode: DEFAULT_MODE,
name: DEFAULT_THEME,
},
}
: {
light: {
mode: DEFAULT_MODE,
name: DEFAULT_THEME,
},
dark: {
mode: otherMode,
name: otherTheme,
},
};

return defaultThemes;
};

0 comments on commit 59ec5cc

Please sign in to comment.