From 93192cf20e232b52ad4a862d198be5a72da4f65b Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 17:37:22 +0530 Subject: [PATCH 01/17] refactor: utils -> function to arrow functions Why? - minify build --- lib/nextjs-themes/src/utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/nextjs-themes/src/utils.ts b/lib/nextjs-themes/src/utils.ts index 0078b198..c1f44449 100644 --- a/lib/nextjs-themes/src/utils.ts +++ b/lib/nextjs-themes/src/utils.ts @@ -1,7 +1,7 @@ import { ThemeSwitcherProps, UpdateProps } from "./client"; import { ColorSchemeType, ThemeStoreType, initialState } from "./constants"; -export function resolveTheme(state?: ThemeStoreType, props?: ThemeSwitcherProps): UpdateProps { +export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps): UpdateProps => { const resolvedForcedTheme = props?.forcedTheme === undefined ? state?.forcedTheme : props.forcedTheme; const resolvedForcedColorScheme = props?.forcedColorScheme === undefined ? state?.forcedColorScheme : props.forcedColorScheme; @@ -30,20 +30,20 @@ export function resolveTheme(state?: ThemeStoreType, props?: ThemeSwitcherProps) return { resolvedTheme, resolvedColorScheme, resolvedColorSchemePref, th }; } -export function getResolvedTheme() { +export const getResolvedTheme = () => { return document.documentElement.getAttribute("data-theme"); } -export function getResolvedColorScheme() { +export const getResolvedColorScheme = () => { return document.documentElement.getAttribute("data-color-scheme"); } -export function encodeState(themeState: ThemeStoreType) { +export const encodeState = (themeState: ThemeStoreType) => { const { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme } = themeState; return [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme].join(","); } -export function parseState(str?: string | null): ThemeStoreType { +export const parseState = (str?: string | null): ThemeStoreType => { if(!str) return initialState; type StrSplitType = [ColorSchemeType, "dark" | "light", string, string, string]; const [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme] = str.split(",") as StrSplitType; From 2e7aad21c9d4c20f068a30ab8d9387c3a0efd925 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 17:39:39 +0530 Subject: [PATCH 02/17] refactor: function to arrow function --- lib/nextjs-themes/src/client/color-switch/color-switch.tsx | 2 +- .../src/client/force-color-scheme/force-color-scheme.tsx | 2 +- lib/nextjs-themes/src/client/force-theme/force-theme.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/nextjs-themes/src/client/color-switch/color-switch.tsx b/lib/nextjs-themes/src/client/color-switch/color-switch.tsx index 9471bf4b..ba9fb41d 100644 --- a/lib/nextjs-themes/src/client/color-switch/color-switch.tsx +++ b/lib/nextjs-themes/src/client/color-switch/color-switch.tsx @@ -22,7 +22,7 @@ export interface ColorSwitchProps { * * ``` */ -export function ColorSwitch({ size = 25, skipSystem }: ColorSwitchProps) { +export const ColorSwitch = ({ size = 25, skipSystem }: ColorSwitchProps) => { const { colorSchemePref, setColorSchemePref } = useTheme(); const toggleColorScheme = () => { switch (colorSchemePref) { diff --git a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx index 5aa2e51e..4beb4929 100644 --- a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx +++ b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx @@ -4,7 +4,7 @@ import { useEffect } from "react"; import type { ColorSchemeType } from "../../constants"; import { useTheme } from "../../hooks"; -export function ForceColorScheme(props: { colorScheme: ColorSchemeType }) { +export const ForceColorScheme = (props: { colorScheme: ColorSchemeType }) => { const { setForcedColorScheme } = useTheme(); useEffect(() => { setForcedColorScheme(props.colorScheme); diff --git a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx index da10db80..ebab6140 100644 --- a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx +++ b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { useEffect } from "react"; import { useTheme } from "../../hooks"; -export function ForceTheme(props: { theme: string }) { +export const ForceTheme = (props: { theme: string }) => { const { setForcedTheme } = useTheme(); useEffect(() => { setForcedTheme(props.theme); From e03c19d6a0ffed4b31a47b8521ce3d51913e5a36 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 17:40:52 +0530 Subject: [PATCH 03/17] refactor: function to arrow function - theme-switcher --- .../src/client/theme-switcher/theme-switcher.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx index 6a050d91..0b295ba6 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -14,7 +14,7 @@ export interface ThemeSwitcherProps { styles?: Record; } -function useMediaQuery(setThemeState: SetStateAction) { +const useMediaQuery = (setThemeState: SetStateAction) => { React.useEffect(() => { // set event listener for media const media = matchMedia("(prefers-color-scheme: dark)"); @@ -30,7 +30,7 @@ function useMediaQuery(setThemeState: SetStateAction) { } let tInit = 0; -function useLoadSyncedState(setThemeState: SetStateAction, targetSelector?: string) { +const useLoadSyncedState = (setThemeState: SetStateAction, targetSelector?: string) => { React.useEffect(() => { tInit = Date.now(); const key = targetSelector ?? DEFAULT_ID; @@ -107,7 +107,7 @@ const disableAnimation = (themeTransition = "none") => { * You can use this hook in place of `` component. * Please note that you need to add "use client" on top of the component in which you are using this hook. */ -export function useThemeSwitcher(props: ThemeSwitcherProps) { +export const useThemeSwitcher = (props: ThemeSwitcherProps) => { const [themeState, setThemeState] = useRGS(props.targetSelector ?? DEFAULT_ID, initialState); useMediaQuery(setThemeState); @@ -131,7 +131,7 @@ export function useThemeSwitcher(props: ThemeSwitcherProps) { * Use this component in your layout - `app/layout.tsx` or your custom layout or in `_app.tsx` file. * @component */ -export function ThemeSwitcher(props: ThemeSwitcherProps) { +export const ThemeSwitcher = (props: ThemeSwitcherProps) => { useThemeSwitcher(props); return null; } From b25cb15758de77ec6c5beae62ade0c3337a3c595 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 17:44:59 +0530 Subject: [PATCH 04/17] refactor: function to arrow function --- lib/nextjs-themes/src/hooks/use-theme.ts | 4 ++-- .../nextjs/server-side-wrapper/server-side-wrapper.tsx | 10 +++++----- .../turbo/generators/templates/component.hbs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index 7ef90467..27a4610e 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -5,7 +5,7 @@ import { useCallback, useMemo } from "react"; const DELAY = 200; // ms - delay to allow reading from localStorage so that local storage does not override the new state const map: Record = {}; -function createSetterWithFirstTimeDelay(setThemeState: SetStateAction) { +const createSetterWithFirstTimeDelay = (setThemeState: SetStateAction) => { return (key: string) => { if (map[key] === undefined) { map[key] = 1; @@ -18,7 +18,7 @@ function createSetterWithFirstTimeDelay(setThemeState: SetStateAction { const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); const { resolvedColorScheme, resolvedTheme } = resolveTheme(themeState); const setterWithFirstTimeDelay = useMemo(() => createSetterWithFirstTimeDelay(setThemeState), []); diff --git a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx index 6be0eaf5..da540b62 100644 --- a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx +++ b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx @@ -20,7 +20,7 @@ export interface NextJsSSRThemeSwitcherProps extends HTMLProps { styles?: Record; } -function getDataProps(resolvedData: UpdateProps, styles?: Record) { +const getDataProps = (resolvedData: UpdateProps, styles?: Record) => { const dataProps: DataProps = { className: "" }; let classeNames = []; if (resolvedData.resolvedColorScheme !== undefined) { @@ -44,10 +44,10 @@ function getDataProps(resolvedData: UpdateProps, styles?: Record return dataProps; } -function sharedServerComponentRenderer( +const sharedServerComponentRenderer = ( { children, tag, forcedPages, targetId, styles, ...props }: NextJsSSRThemeSwitcherProps, defaultTag: "div" | "html", -) { +) => { const Tag: keyof JSX.IntrinsicElements = tag || defaultTag; const key = targetId ? `#${targetId}` : DEFAULT_ID; const state = cookies().get(key)?.value; @@ -78,7 +78,7 @@ function sharedServerComponentRenderer( * * ``` */ -export function NextJsSSGThemeSwitcher(props: NextJsSSRThemeSwitcherProps) { +export const NextJsSSGThemeSwitcher = (props: NextJsSSRThemeSwitcherProps) => { return sharedServerComponentRenderer(props, "div"); } @@ -102,6 +102,6 @@ export interface ServerSideWrapperProps extends NextJsSSRThemeSwitcherProps { * * ``` */ -export function ServerSideWrapper(props: ServerSideWrapperProps) { +export const ServerSideWrapper = (props: ServerSideWrapperProps) => { return sharedServerComponentRenderer(props, "html"); } diff --git a/lib/nextjs-themes/turbo/generators/templates/component.hbs b/lib/nextjs-themes/turbo/generators/templates/component.hbs index 2dd8909e..59d62c0d 100644 --- a/lib/nextjs-themes/turbo/generators/templates/component.hbs +++ b/lib/nextjs-themes/turbo/generators/templates/component.hbs @@ -9,7 +9,7 @@ interface {{ pascalCase name }}Props { * @example * <{{pascalCase name}} /> */ -export function {{ pascalCase name }}({ children }: {{ pascalCase name }}Props) { +export const {{ pascalCase name }} = ({ children }: {{ pascalCase name }}Props) => { return (

{{ name }}

From 3824f5ed7a61b1207ad995be5a3f321cdc0b8c00 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 17:48:24 +0530 Subject: [PATCH 05/17] fix: remove unnecessary iife --- .../src/client/theme-switcher/theme-switcher.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx index 0b295ba6..879e2f72 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -41,9 +41,9 @@ const useLoadSyncedState = (setThemeState: SetStateAction, targe const storageListener = (e: StorageEvent) => { if (e.key === key) setThemeState(state => ({ ...state, ...parseState(e.newValue) })); }; - window.addEventListener("storage", storageListener); + addEventListener("storage", storageListener); return () => { - window.removeEventListener("storage", storageListener); + removeEventListener("storage", storageListener); }; }, [targetSelector]); } @@ -95,7 +95,7 @@ const disableAnimation = (themeTransition = "none") => { return () => { // Force restyle - (() => window.getComputedStyle(document.body))(); + getComputedStyle(document.body); // Wait for next tick before removing setTimeout(() => { document.head.removeChild(css); From 2f311f02a274d18551c56129cf44aec6515f4288 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 18:55:00 +0530 Subject: [PATCH 06/17] format --- .../src/client/color-switch/color-switch.tsx | 2 +- .../force-color-scheme/force-color-scheme.tsx | 2 +- .../src/client/force-theme/force-theme.tsx | 2 +- .../client/theme-switcher/theme-switcher.tsx | 8 ++--- lib/nextjs-themes/src/hooks/use-theme.ts | 4 +-- .../server-side-wrapper.test.tsx | 2 +- .../server-side-wrapper.tsx | 8 ++--- lib/nextjs-themes/src/utils.ts | 29 ++++++++++++++----- 8 files changed, 35 insertions(+), 22 deletions(-) diff --git a/lib/nextjs-themes/src/client/color-switch/color-switch.tsx b/lib/nextjs-themes/src/client/color-switch/color-switch.tsx index ba9fb41d..14db28bd 100644 --- a/lib/nextjs-themes/src/client/color-switch/color-switch.tsx +++ b/lib/nextjs-themes/src/client/color-switch/color-switch.tsx @@ -47,4 +47,4 @@ export const ColorSwitch = ({ size = 25, skipSystem }: ColorSwitchProps) => { style={{ "--size": `${size}px` }} /> ); -} +}; diff --git a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx index 4beb4929..a218e33b 100644 --- a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx +++ b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx @@ -13,4 +13,4 @@ export const ForceColorScheme = (props: { colorScheme: ColorSchemeType }) => { }; }, [props.colorScheme]); return null; -} +}; diff --git a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx index ebab6140..efff8f11 100644 --- a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx +++ b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx @@ -12,4 +12,4 @@ export const ForceTheme = (props: { theme: string }) => { }; }, [props.theme]); return null; -} +}; diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx index 879e2f72..cf964362 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -27,7 +27,7 @@ const useMediaQuery = (setThemeState: SetStateAction) => { media.removeEventListener("change", updateSystemColorScheme); }; }, [setThemeState]); -} +}; let tInit = 0; const useLoadSyncedState = (setThemeState: SetStateAction, targetSelector?: string) => { @@ -46,7 +46,7 @@ const useLoadSyncedState = (setThemeState: SetStateAction, targe removeEventListener("storage", storageListener); }; }, [targetSelector]); -} +}; export interface DataProps { className: string; @@ -125,7 +125,7 @@ export const useThemeSwitcher = (props: ThemeSwitcherProps) => { } restoreTransitions(); }, [props, themeState]); -} +}; /** * Use this component in your layout - `app/layout.tsx` or your custom layout or in `_app.tsx` file. @@ -134,4 +134,4 @@ export const useThemeSwitcher = (props: ThemeSwitcherProps) => { export const ThemeSwitcher = (props: ThemeSwitcherProps) => { useThemeSwitcher(props); return null; -} +}; diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index 27a4610e..4a45f362 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -16,7 +16,7 @@ const createSetterWithFirstTimeDelay = (setThemeState: SetStateAction void; }; -} +}; export const useTheme = (targetId?: string) => { const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); @@ -37,4 +37,4 @@ export const useTheme = (targetId?: string) => { setForcedTheme: setterWithFirstTimeDelay("forcedTheme"), setForcedColorScheme: setterWithFirstTimeDelay("forcedColorScheme"), }; -} +}; diff --git a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx index af518be3..e8b097c8 100644 --- a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx +++ b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx @@ -15,7 +15,7 @@ describe("server-side-target", () => { darkTheme: "dark-blue", lightTheme: "light-yellow", colorSchemePref: "dark", - systemColorScheme: "dark" + systemColorScheme: "dark", }), }, }; diff --git a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx index da540b62..50f1a284 100644 --- a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx +++ b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx @@ -42,7 +42,7 @@ const getDataProps = (resolvedData: UpdateProps, styles?: Record if (styles) classeNames = classeNames.map(cls => styles[cls] ?? cls); dataProps.className = classeNames.join(" "); return dataProps; -} +}; const sharedServerComponentRenderer = ( { children, tag, forcedPages, targetId, styles, ...props }: NextJsSSRThemeSwitcherProps, @@ -70,7 +70,7 @@ const sharedServerComponentRenderer = ( {children} ); -} +}; /** * @example @@ -80,7 +80,7 @@ const sharedServerComponentRenderer = ( */ export const NextJsSSGThemeSwitcher = (props: NextJsSSRThemeSwitcherProps) => { return sharedServerComponentRenderer(props, "div"); -} +}; /** For naming consistancy, clarity, and minimizing API updates */ export { NextJsSSGThemeSwitcher as NextJsServerTarget }; @@ -104,4 +104,4 @@ export interface ServerSideWrapperProps extends NextJsSSRThemeSwitcherProps { */ export const ServerSideWrapper = (props: ServerSideWrapperProps) => { return sharedServerComponentRenderer(props, "html"); -} +}; diff --git a/lib/nextjs-themes/src/utils.ts b/lib/nextjs-themes/src/utils.ts index c1f44449..48b56c40 100644 --- a/lib/nextjs-themes/src/utils.ts +++ b/lib/nextjs-themes/src/utils.ts @@ -1,5 +1,6 @@ +import useRGS from "r18gs"; import { ThemeSwitcherProps, UpdateProps } from "./client"; -import { ColorSchemeType, ThemeStoreType, initialState } from "./constants"; +import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "./constants"; export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps): UpdateProps => { const resolvedForcedTheme = props?.forcedTheme === undefined ? state?.forcedTheme : props.forcedTheme; @@ -28,24 +29,36 @@ export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps) const th = resolvedForcedTheme === undefined ? state?.theme || "" : resolvedForcedTheme; return { resolvedTheme, resolvedColorScheme, resolvedColorSchemePref, th }; -} +}; export const getResolvedTheme = () => { return document.documentElement.getAttribute("data-theme"); -} +}; export const getResolvedColorScheme = () => { return document.documentElement.getAttribute("data-color-scheme"); -} +}; export const encodeState = (themeState: ThemeStoreType) => { const { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme } = themeState; return [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme].join(","); -} +}; +type StrSplitType = [ColorSchemeType, "dark" | "light", string, string, string]; export const parseState = (str?: string | null): ThemeStoreType => { - if(!str) return initialState; - type StrSplitType = [ColorSchemeType, "dark" | "light", string, string, string]; + if (!str) return initialState; const [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme] = str.split(",") as StrSplitType; return { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme }; -} +}; + +export const useStore = (targetId: string) => { + const key = targetId ?? DEFAULT_ID; + const [] = useRGS(key, () => { + const str = typeof localStorage !== "undefined" ? localStorage.getItem(key) : null; + if (str) { + const [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme] = str.split(",") as StrSplitType; + return { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme }; + } + return initialState; + }); +}; From 46c3b1ca9c5a1c75a14dae2e1e87a07628e267c1 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 21:28:33 +0530 Subject: [PATCH 07/17] simplify --- .../client/theme-switcher/theme-switcher.tsx | 83 ++++++------------- lib/nextjs-themes/src/constants.ts | 3 + lib/nextjs-themes/src/utils.ts | 52 ++++-------- 3 files changed, 43 insertions(+), 95 deletions(-) diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx index cf964362..d1a62667 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -1,10 +1,7 @@ -import * as React from "react"; import { useEffect } from "react"; -import { resolveTheme, parseState, encodeState } from "../../utils"; -import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../../constants"; -import useRGS, { SetStateAction } from "r18gs"; +import { resolveTheme, media, useStore } from "../../utils"; +import { ColorSchemeType, DEFAULT_ID } from "../../constants"; -/** todo - set persistance and cookies */ export interface ThemeSwitcherProps { forcedTheme?: string; forcedColorScheme?: ColorSchemeType; @@ -14,46 +11,13 @@ export interface ThemeSwitcherProps { styles?: Record; } -const useMediaQuery = (setThemeState: SetStateAction) => { - React.useEffect(() => { - // set event listener for media - const media = matchMedia("(prefers-color-scheme: dark)"); - const updateSystemColorScheme = () => { - setThemeState(state => ({ ...state, systemColorScheme: media.matches ? "dark" : "light" })); - }; - updateSystemColorScheme(); - media.addEventListener("change", updateSystemColorScheme); - return () => { - media.removeEventListener("change", updateSystemColorScheme); - }; - }, [setThemeState]); -}; - -let tInit = 0; -const useLoadSyncedState = (setThemeState: SetStateAction, targetSelector?: string) => { - React.useEffect(() => { - tInit = Date.now(); - const key = targetSelector ?? DEFAULT_ID; - const storedState = parseState(localStorage.getItem(key)); - // @ts-ignore - delete storedState.systemColorScheme; - setThemeState(state => ({ ...state, ...storedState })); - const storageListener = (e: StorageEvent) => { - if (e.key === key) setThemeState(state => ({ ...state, ...parseState(e.newValue) })); - }; - addEventListener("storage", storageListener); - return () => { - removeEventListener("storage", storageListener); - }; - }, [targetSelector]); -}; - export interface DataProps { className: string; "data-th"?: string; "data-theme"?: string; "data-color-scheme"?: "dark" | "light"; - "data-csp"?: ColorSchemeType /** color-scheme-preference */; + /** color-scheme-preference */ + "data-csp"?: ColorSchemeType; } export interface UpdateProps { @@ -84,22 +48,18 @@ const updateDOM = ( return shouldCreateCookie; }; -const disableAnimation = (themeTransition = "none") => { +/** disable transition while switching theme */ +const disableTransition = (themeTransition = "none") => { const css = document.createElement("style"); /** split by ';' to prevent CSS injection */ - const transition = `transition: ${themeTransition.split(";")[0]} !important;`; - css.appendChild( - document.createTextNode(`*{-webkit-${transition}-moz-${transition}-o-${transition}-ms-${transition}${transition}}`), - ); + css.textContent = `transition: ${themeTransition.split(";")[0]} !important;`; document.head.appendChild(css); return () => { // Force restyle getComputedStyle(document.body); // Wait for next tick before removing - setTimeout(() => { - document.head.removeChild(css); - }, 1); + setTimeout(() => document.head.removeChild(css), 1); }; }; @@ -108,21 +68,26 @@ const disableAnimation = (themeTransition = "none") => { * Please note that you need to add "use client" on top of the component in which you are using this hook. */ export const useThemeSwitcher = (props: ThemeSwitcherProps) => { - const [themeState, setThemeState] = useRGS(props.targetSelector ?? DEFAULT_ID, initialState); - - useMediaQuery(setThemeState); - useLoadSyncedState(setThemeState, props.targetSelector); + const [themeState, setThemeState] = useStore(props.targetSelector); + // not using ?? as we don't want key to be an empty string ever + const key = props.targetSelector || DEFAULT_ID; + /** set listeners for system preference and syncing store */ useEffect(() => { - const restoreTransitions = disableAnimation(props.themeTransition); + media.addEventListener("change", () => + setThemeState(state => ({ ...state, systemColorScheme: media.matches ? "dark" : "light" })), + ); + addEventListener("storage", e => { + if (e.key === key) setThemeState(state => ({ ...state, ...JSON.parse(e.newValue || "{}") })); + }); + }, []); + useEffect(() => { + const restoreTransitions = disableTransition(props.themeTransition); const resolvedData = resolveTheme(themeState, props); const shouldCreateCookie = updateDOM(resolvedData, props); - if (tInit < Date.now() - 300) { - const stateStr = encodeState(themeState); - const key = props.targetSelector || DEFAULT_ID; - localStorage.setItem(key, stateStr); - if (shouldCreateCookie) document.cookie = `${key}=${stateStr}; max-age=31536000; SameSite=Strict;`; - } + const stateStr = JSON.stringify(themeState); + localStorage.setItem(key, stateStr); + if (shouldCreateCookie) document.cookie = `${key}=${stateStr};max-age=31536000;SameSite=Strict;`; restoreTransitions(); }, [props, themeState]); }; diff --git a/lib/nextjs-themes/src/constants.ts b/lib/nextjs-themes/src/constants.ts index 136769c2..5c67928b 100644 --- a/lib/nextjs-themes/src/constants.ts +++ b/lib/nextjs-themes/src/constants.ts @@ -1,7 +1,10 @@ /** shared constants -- keep in separate files for better tree-shaking and dependency injection */ export const DEFAULT_ID = "nth"; +export const LIGHT = "light"; +export const DARK = "dark"; export type ColorSchemeType = "" | "system" | "dark" | "light"; +export type ResolvedColorSchemeType = "dark" | "light"; export interface ThemeStoreType { theme: string; diff --git a/lib/nextjs-themes/src/utils.ts b/lib/nextjs-themes/src/utils.ts index 48b56c40..041772d6 100644 --- a/lib/nextjs-themes/src/utils.ts +++ b/lib/nextjs-themes/src/utils.ts @@ -1,18 +1,18 @@ import useRGS from "r18gs"; import { ThemeSwitcherProps, UpdateProps } from "./client"; -import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "./constants"; +import { DARK, DEFAULT_ID, ResolvedColorSchemeType, ThemeStoreType, initialState } from "./constants"; +/** resolve props and state to a final attributes that should be applied to the DOM */ export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps): UpdateProps => { - const resolvedForcedTheme = props?.forcedTheme === undefined ? state?.forcedTheme : props.forcedTheme; - const resolvedForcedColorScheme = - props?.forcedColorScheme === undefined ? state?.forcedColorScheme : props.forcedColorScheme; - const resolvedColorSchemePref = - (resolvedForcedColorScheme === undefined ? state?.colorSchemePref : resolvedForcedColorScheme) || ""; + const resolvedForcedTheme = props?.forcedTheme ?? state?.forcedTheme; + const resolvedForcedColorScheme = props?.forcedColorScheme ?? state?.forcedColorScheme; + const resolvedColorSchemePref = (resolvedForcedColorScheme ?? state?.colorSchemePref) || ""; - const isSystemDark = state?.systemColorScheme === "dark"; + const isSystemDark = state?.systemColorScheme === DARK; - let resolvedColorScheme: "dark" | "light" = isSystemDark ? "dark" : "light"; - let resolvedTheme = resolvedForcedTheme === undefined ? state?.theme || "" : resolvedForcedTheme; + /** these will be modified in the switch statement */ + let resolvedColorScheme: ResolvedColorSchemeType = isSystemDark ? "dark" : "light"; + let resolvedTheme = (resolvedForcedTheme ?? state?.theme) || ""; if (resolvedForcedTheme === undefined) switch (resolvedColorSchemePref) { @@ -31,34 +31,14 @@ export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps) return { resolvedTheme, resolvedColorScheme, resolvedColorSchemePref, th }; }; -export const getResolvedTheme = () => { - return document.documentElement.getAttribute("data-theme"); -}; - -export const getResolvedColorScheme = () => { - return document.documentElement.getAttribute("data-color-scheme"); -}; - -export const encodeState = (themeState: ThemeStoreType) => { - const { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme } = themeState; - return [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme].join(","); -}; - -type StrSplitType = [ColorSchemeType, "dark" | "light", string, string, string]; -export const parseState = (str?: string | null): ThemeStoreType => { - if (!str) return initialState; - const [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme] = str.split(",") as StrSplitType; - return { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme }; -}; +const isServer = typeof localStorage === "undefined"; +export const media = matchMedia("(prefers-color-scheme: dark)"); -export const useStore = (targetId: string) => { +/** internal store API */ +export const useStore = (targetId?: string) => { const key = targetId ?? DEFAULT_ID; - const [] = useRGS(key, () => { - const str = typeof localStorage !== "undefined" ? localStorage.getItem(key) : null; - if (str) { - const [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme] = str.split(",") as StrSplitType; - return { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme }; - } - return initialState; + return useRGS(key, () => { + const str = isServer ? null : localStorage.getItem(key); + return str ? { ...JSON.parse(str), systemColorScheme: media.matches ? "dark" : "light" } : initialState; }); }; From d399e17c325a33cdd132ae16adc699a99c42091a Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 21:58:13 +0530 Subject: [PATCH 08/17] reduce magic numbers --- .../client/color-switch/color-switch.test.tsx | 9 +++++---- .../src/client/color-switch/color-switch.tsx | 13 +++++++------ .../force-color-scheme.test.tsx | 7 ++++--- .../theme-switcher/theme-switcher.test.tsx | 8 ++++---- .../src/client/theme-switcher/theme-switcher.tsx | 8 ++++---- lib/nextjs-themes/src/constants.ts | 7 ++++--- lib/nextjs-themes/src/utils.ts | 16 ++++++++-------- 7 files changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/nextjs-themes/src/client/color-switch/color-switch.test.tsx b/lib/nextjs-themes/src/client/color-switch/color-switch.test.tsx index e7140ab3..3b238e92 100644 --- a/lib/nextjs-themes/src/client/color-switch/color-switch.test.tsx +++ b/lib/nextjs-themes/src/client/color-switch/color-switch.test.tsx @@ -2,6 +2,7 @@ import { act, cleanup, fireEvent, render, renderHook, screen } from "@testing-li import { ColorSwitch } from "./color-switch"; import { useTheme } from "../../hooks"; import { afterEach, describe, test } from "vitest"; +import { DARK, LIGHT, SYSTEM } from "../../constants"; describe("color-switch", () => { afterEach(cleanup); @@ -12,12 +13,12 @@ describe("color-switch", () => { render(); const element = screen.getByTestId("color-switch"); act(() => fireEvent.click(element)); - expect(hook.result.current.colorSchemePref).toBe("dark"); + expect(hook.result.current.colorSchemePref).toBe(DARK); act(() => fireEvent.click(element)); - expect(hook.result.current.colorSchemePref).toBe("light"); + expect(hook.result.current.colorSchemePref).toBe(LIGHT); act(() => fireEvent.click(element)); - expect(hook.result.current.colorSchemePref).toBe("system"); + expect(hook.result.current.colorSchemePref).toBe(SYSTEM); act(() => fireEvent.click(element)); - expect(hook.result.current.colorSchemePref).toBe("dark"); + expect(hook.result.current.colorSchemePref).toBe(DARK); }); }); diff --git a/lib/nextjs-themes/src/client/color-switch/color-switch.tsx b/lib/nextjs-themes/src/client/color-switch/color-switch.tsx index 14db28bd..7619ea7e 100644 --- a/lib/nextjs-themes/src/client/color-switch/color-switch.tsx +++ b/lib/nextjs-themes/src/client/color-switch/color-switch.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { useTheme } from "../../hooks"; +import { DARK, LIGHT, SYSTEM } from "../../constants"; export interface ColorSwitchProps { /** Diameter of the color switch */ @@ -27,14 +28,14 @@ export const ColorSwitch = ({ size = 25, skipSystem }: ColorSwitchProps) => { const toggleColorScheme = () => { switch (colorSchemePref) { case "": - case "system": - setColorSchemePref("dark"); + case SYSTEM: + setColorSchemePref(DARK); break; - case "dark": - setColorSchemePref("light"); + case DARK: + setColorSchemePref(LIGHT); break; - case "light": - setColorSchemePref(skipSystem ? "dark" : "system"); + case LIGHT: + setColorSchemePref(skipSystem ? DARK : SYSTEM); } }; return ( diff --git a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.test.tsx b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.test.tsx index d6fefe65..7ffef8b1 100644 --- a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.test.tsx +++ b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.test.tsx @@ -2,15 +2,16 @@ import { act, cleanup, render, renderHook } from "@testing-library/react"; import { afterEach, describe, test } from "vitest"; import { useTheme } from "../../hooks"; import { ForceColorScheme } from "./force-color-scheme"; +import { DARK, LIGHT } from "../../constants"; describe.concurrent("force-color-scheme", () => { afterEach(cleanup); /** Test only the things that this component is responsible for - chanding state*/ test("Force theme with force color scheme", async ({ expect }) => { const { result } = renderHook(() => useTheme()); - act(() => result.current.setForcedColorScheme("light")); - const { unmount } = await act(() => render()); - expect(result.current.forcedColorScheme).toBe("dark"); + act(() => result.current.setForcedColorScheme(LIGHT)); + const { unmount } = await act(() => render()); + expect(result.current.forcedColorScheme).toBe(DARK); act(() => unmount()); expect(result.current.forcedColorScheme).toBe(undefined); }); diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx index 86cc1be7..d092af69 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx @@ -4,7 +4,7 @@ import { useTheme } from "../../hooks"; import { ThemeSwitcher } from "./theme-switcher"; import { encodeState, getResolvedColorScheme, getResolvedTheme } from "../../utils"; import useRGS, { SetterArgType } from "r18gs"; -import { DEFAULT_ID, ThemeStoreType, initialState } from "../../constants"; +import { DARK, DEFAULT_ID, LIGHT, ThemeStoreType, initialState } from "../../constants"; /** * -> concurrency is not feasible because of global store conflicts @@ -35,7 +35,7 @@ describe("theme-switcher", () => { test("Test defaultDark theme", async ({ expect }) => { const darkTheme = "dark1"; /** simulate system dark mode */ - act(() => rgsHook.result.current[1](state => ({ ...state, systemColorScheme: "dark" }))); + act(() => rgsHook.result.current[1](state => ({ ...state, systemColorScheme: DARK }))); /** simulate changing darkTheme by another component by user */ await new Promise(res => setTimeout(res, 350)); const { result } = renderHook(() => useTheme()); @@ -56,13 +56,13 @@ describe("theme-switcher", () => { test("test color scheme preference", async ({ expect }) => { const { result } = renderHook(() => useTheme()); - act(() => result.current.setColorSchemePref("light")); + act(() => result.current.setColorSchemePref(LIGHT)); act(() => result.current.setLightTheme("yellow")); act(() => result.current.setDarkTheme("dark-blue")); act(() => result.current.setTheme("blue")); await new Promise(res => setTimeout(res, 250)); expect(getResolvedTheme()).toBe("yellow"); - act(() => result.current.setColorSchemePref("dark")); + act(() => result.current.setColorSchemePref(DARK)); /** note we do not require to await second time -- ?? what is user is setting theme from multuple useEffects? */ expect(getResolvedTheme()).toBe("dark-blue"); }); diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx index d1a62667..0dd39c9f 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { resolveTheme, media, useStore } from "../../utils"; -import { ColorSchemeType, DEFAULT_ID } from "../../constants"; +import { ColorSchemeType, DARK, DEFAULT_ID, LIGHT, ResolvedColorSchemeType } from "../../constants"; export interface ThemeSwitcherProps { forcedTheme?: string; @@ -15,14 +15,14 @@ export interface DataProps { className: string; "data-th"?: string; "data-theme"?: string; - "data-color-scheme"?: "dark" | "light"; + "data-color-scheme"?: ResolvedColorSchemeType; /** color-scheme-preference */ "data-csp"?: ColorSchemeType; } export interface UpdateProps { resolvedTheme: string; - resolvedColorScheme: "dark" | "light"; + resolvedColorScheme: ResolvedColorSchemeType; resolvedColorSchemePref: ColorSchemeType; th: string; } @@ -74,7 +74,7 @@ export const useThemeSwitcher = (props: ThemeSwitcherProps) => { /** set listeners for system preference and syncing store */ useEffect(() => { media.addEventListener("change", () => - setThemeState(state => ({ ...state, systemColorScheme: media.matches ? "dark" : "light" })), + setThemeState(state => ({ ...state, systemColorScheme: media.matches ? DARK : LIGHT })), ); addEventListener("storage", e => { if (e.key === key) setThemeState(state => ({ ...state, ...JSON.parse(e.newValue || "{}") })); diff --git a/lib/nextjs-themes/src/constants.ts b/lib/nextjs-themes/src/constants.ts index 5c67928b..2ccac66b 100644 --- a/lib/nextjs-themes/src/constants.ts +++ b/lib/nextjs-themes/src/constants.ts @@ -2,6 +2,7 @@ export const DEFAULT_ID = "nth"; export const LIGHT = "light"; export const DARK = "dark"; +export const SYSTEM = "system"; export type ColorSchemeType = "" | "system" | "dark" | "light"; export type ResolvedColorSchemeType = "dark" | "light"; @@ -28,8 +29,8 @@ export type ThemeStoreActionsType = { export const initialState: ThemeStoreType = { theme: "", - darkTheme: "dark", + darkTheme: DARK, lightTheme: "", - colorSchemePref: "system", - systemColorScheme: "light", + colorSchemePref: SYSTEM, + systemColorScheme: LIGHT, }; diff --git a/lib/nextjs-themes/src/utils.ts b/lib/nextjs-themes/src/utils.ts index 041772d6..c6d5ec4d 100644 --- a/lib/nextjs-themes/src/utils.ts +++ b/lib/nextjs-themes/src/utils.ts @@ -1,6 +1,6 @@ import useRGS from "r18gs"; import { ThemeSwitcherProps, UpdateProps } from "./client"; -import { DARK, DEFAULT_ID, ResolvedColorSchemeType, ThemeStoreType, initialState } from "./constants"; +import { DARK, DEFAULT_ID, LIGHT, ResolvedColorSchemeType, SYSTEM, ThemeStoreType, initialState } from "./constants"; /** resolve props and state to a final attributes that should be applied to the DOM */ export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps): UpdateProps => { @@ -11,19 +11,19 @@ export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps) const isSystemDark = state?.systemColorScheme === DARK; /** these will be modified in the switch statement */ - let resolvedColorScheme: ResolvedColorSchemeType = isSystemDark ? "dark" : "light"; + let resolvedColorScheme: ResolvedColorSchemeType = isSystemDark ? DARK : LIGHT; let resolvedTheme = (resolvedForcedTheme ?? state?.theme) || ""; if (resolvedForcedTheme === undefined) switch (resolvedColorSchemePref) { - case "system": + case SYSTEM: resolvedTheme = (isSystemDark ? state?.darkTheme : state?.lightTheme) || ""; break; - case "dark": - [resolvedTheme, resolvedColorScheme] = [state?.darkTheme || "", "dark"]; + case DARK: + [resolvedTheme, resolvedColorScheme] = [state?.darkTheme || "", DARK]; break; - case "light": - [resolvedTheme, resolvedColorScheme] = [state?.lightTheme || "", "light"]; + case LIGHT: + [resolvedTheme, resolvedColorScheme] = [state?.lightTheme || "", LIGHT]; break; } @@ -39,6 +39,6 @@ export const useStore = (targetId?: string) => { const key = targetId ?? DEFAULT_ID; return useRGS(key, () => { const str = isServer ? null : localStorage.getItem(key); - return str ? { ...JSON.parse(str), systemColorScheme: media.matches ? "dark" : "light" } : initialState; + return str ? { ...JSON.parse(str), systemColorScheme: media.matches ? DARK : LIGHT } : initialState; }); }; From 393ba96abd80973dee037ef830a24c6d9ac071a3 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 22:01:31 +0530 Subject: [PATCH 09/17] fix: fix build --- .../server/nextjs/server-side-wrapper/server-side-wrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx index 50f1a284..6b651089 100644 --- a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx +++ b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import type { HTMLProps, ReactNode } from "react"; import { cookies, headers } from "next/headers"; import { DEFAULT_ID, type ColorSchemeType, type ThemeStoreType } from "../../../constants"; -import { parseState, resolveTheme } from "../../../utils"; +import { resolveTheme } from "../../../utils"; import { DataProps, UpdateProps } from "../../../client"; export type ForcedPage = @@ -59,7 +59,7 @@ const sharedServerComponentRenderer = ( const forcedPageProps = Array.isArray(forcedPage) ? { forcedTheme: forcedPage[1].theme, forcedColorScheme: forcedPage[1].colorScheme } : forcedPage?.props; - const themeState = state ? (parseState(state) as ThemeStoreType) : undefined; + const themeState = state ? (JSON.parse(state) as ThemeStoreType) : undefined; const resolvedData = resolveTheme(themeState, forcedPageProps); const dataProps = getDataProps(resolvedData, styles); if (targetId) dataProps.className += styles?.[" nth-scoped"] ?? " nth-scoped"; From 7049636f6413fce8ad1c55e199b451024f339c4f Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 22:02:35 +0530 Subject: [PATCH 10/17] fix server tests --- .../nextjs/server-side-wrapper/server-side-wrapper.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx index e8b097c8..22d492a6 100644 --- a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx +++ b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.test.tsx @@ -2,7 +2,6 @@ import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, test, beforeEach } from "vitest"; import { NextJsSSGThemeSwitcher, ServerSideWrapper } from "."; import { DEFAULT_ID } from "../../../constants"; -import { encodeState } from "../../../utils"; describe("server-side-target", () => { afterEach(cleanup); @@ -10,7 +9,7 @@ describe("server-side-target", () => { beforeEach(() => { globalThis.cookies = { [DEFAULT_ID]: { - value: encodeState({ + value: JSON.stringify({ theme: "yellow", darkTheme: "dark-blue", lightTheme: "light-yellow", From 8ce0feb83c7eda15c7733830fd99068bbfb735bb Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 22:09:16 +0530 Subject: [PATCH 11/17] fix: theme-switcher tests --- .../theme-switcher/theme-switcher.test.tsx | 25 ++++++++++++++++--- lib/nextjs-themes/vitest.setup.ts | 11 +++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx index d092af69..7ea10aad 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx @@ -2,9 +2,14 @@ import { RenderHookResult, act, cleanup, fireEvent, render, renderHook } from "@ import { afterEach, beforeEach, describe, test } from "vitest"; import { useTheme } from "../../hooks"; import { ThemeSwitcher } from "./theme-switcher"; -import { encodeState, getResolvedColorScheme, getResolvedTheme } from "../../utils"; import useRGS, { SetterArgType } from "r18gs"; import { DARK, DEFAULT_ID, LIGHT, ThemeStoreType, initialState } from "../../constants"; +import { media } from "../../utils"; + +const getResolvedTheme = () => { + const theme = document.documentElement.getAttribute("data-theme"); + return theme; +}; /** * -> concurrency is not feasible because of global store conflicts @@ -89,7 +94,6 @@ describe("theme-switcher", () => { expect(getResolvedTheme()).toBe(""); act(() => result.current.setForcedTheme(undefined)); expect(getResolvedTheme()).toBe("black"); - expect(getResolvedColorScheme()).toBe("dark"); }); test("Storage event", async ({ expect }) => { @@ -98,7 +102,10 @@ describe("theme-switcher", () => { await act(() => fireEvent( window, - new StorageEvent("storage", { key: DEFAULT_ID, newValue: encodeState({ ...initialState, theme: MY_THEME }) }), + new StorageEvent("storage", { + key: DEFAULT_ID, + newValue: JSON.stringify({ ...initialState, theme: MY_THEME }), + }), ), ); expect(hook.result.current.theme).toBe(MY_THEME); @@ -125,6 +132,16 @@ describe("theme-switcher with props", () => { test("forced colorScheme prop", async ({ expect }) => { // global state is continuing from previous testss await act(() => render()); - expect(getResolvedColorScheme()).toBe("light"); + expect(getResolvedTheme()).toBe(""); + }); + + test("media change event", async ({ expect }) => { + await act(() => render()); + await act(() => { + // globalThis.window.media = LIGHT as ResolvedScheme; + // @ts-expect-error -- ok + media.onchange?.(); + }); + expect(getResolvedTheme()).toBe(DARK); }); }); diff --git a/lib/nextjs-themes/vitest.setup.ts b/lib/nextjs-themes/vitest.setup.ts index 42546a0b..d6a1f43f 100644 --- a/lib/nextjs-themes/vitest.setup.ts +++ b/lib/nextjs-themes/vitest.setup.ts @@ -1,14 +1,19 @@ import { vi } from "vitest"; +const mediaListeners: (() => void)[] = []; // mock matchMedia Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: query.includes(window.media), media: query, - onchange: null, - addEventListener: vi.fn(), - removeEventListener: vi.fn(), + onchange() { + this.matches = query.includes(window.media); + mediaListeners.forEach(listener => listener()); + }, + addEventListener: (_: string, listener: () => void) => mediaListeners.push(listener), + removeEventListener: (_: string, listener: () => void) => + mediaListeners.splice(mediaListeners.indexOf(listener), 1), dispatchEvent: vi.fn(), })), }); From 895205472661e608b2b14a705957f5dca0eb9811 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 22:28:47 +0530 Subject: [PATCH 12/17] fix build --- examples/app-router/app/layout.tsx | 1 + .../src/client/theme-switcher/theme-switcher.test.tsx | 5 +++-- .../src/client/theme-switcher/theme-switcher.tsx | 3 ++- lib/nextjs-themes/src/constants.ts | 2 +- lib/nextjs-themes/src/utils.ts | 4 ++-- packages/shared-ui/src/globals.css | 2 ++ 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/app-router/app/layout.tsx b/examples/app-router/app/layout.tsx index 1356526c..01f07d4b 100644 --- a/examples/app-router/app/layout.tsx +++ b/examples/app-router/app/layout.tsx @@ -3,6 +3,7 @@ import { NextJsSSGThemeSwitcher } from "nextjs-themes/server"; import { Inter } from "next/font/google"; import { SharedRootLayout, darkThemes, lightThemes } from "shared-ui"; import Link from "next/link"; +import "nextjs-themes/src/styles.css"; const inter = Inter({ subsets: ["latin"] }); const forcedPages: ForcedPage[] = [ diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx index 7ea10aad..3ff4668d 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx @@ -4,8 +4,9 @@ import { useTheme } from "../../hooks"; import { ThemeSwitcher } from "./theme-switcher"; import useRGS, { SetterArgType } from "r18gs"; import { DARK, DEFAULT_ID, LIGHT, ThemeStoreType, initialState } from "../../constants"; -import { media } from "../../utils"; +import { MEDIA } from "../../utils"; +/** get dom attribute */ const getResolvedTheme = () => { const theme = document.documentElement.getAttribute("data-theme"); return theme; @@ -140,7 +141,7 @@ describe("theme-switcher with props", () => { await act(() => { // globalThis.window.media = LIGHT as ResolvedScheme; // @ts-expect-error -- ok - media.onchange?.(); + matchMedia(MEDIA).onchange?.(); }); expect(getResolvedTheme()).toBe(DARK); }); diff --git a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx index 0dd39c9f..836c602d 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { resolveTheme, media, useStore } from "../../utils"; +import { resolveTheme, MEDIA, useStore } from "../../utils"; import { ColorSchemeType, DARK, DEFAULT_ID, LIGHT, ResolvedColorSchemeType } from "../../constants"; export interface ThemeSwitcherProps { @@ -73,6 +73,7 @@ export const useThemeSwitcher = (props: ThemeSwitcherProps) => { const key = props.targetSelector || DEFAULT_ID; /** set listeners for system preference and syncing store */ useEffect(() => { + const media = matchMedia(MEDIA); media.addEventListener("change", () => setThemeState(state => ({ ...state, systemColorScheme: media.matches ? DARK : LIGHT })), ); diff --git a/lib/nextjs-themes/src/constants.ts b/lib/nextjs-themes/src/constants.ts index 2ccac66b..fad01b90 100644 --- a/lib/nextjs-themes/src/constants.ts +++ b/lib/nextjs-themes/src/constants.ts @@ -1,5 +1,5 @@ /** shared constants -- keep in separate files for better tree-shaking and dependency injection */ -export const DEFAULT_ID = "nth"; +export const DEFAULT_ID = "nth-1"; export const LIGHT = "light"; export const DARK = "dark"; export const SYSTEM = "system"; diff --git a/lib/nextjs-themes/src/utils.ts b/lib/nextjs-themes/src/utils.ts index c6d5ec4d..61065fc4 100644 --- a/lib/nextjs-themes/src/utils.ts +++ b/lib/nextjs-themes/src/utils.ts @@ -32,13 +32,13 @@ export const resolveTheme = (state?: ThemeStoreType, props?: ThemeSwitcherProps) }; const isServer = typeof localStorage === "undefined"; -export const media = matchMedia("(prefers-color-scheme: dark)"); +export const MEDIA = "(prefers-color-scheme: dark)"; /** internal store API */ export const useStore = (targetId?: string) => { const key = targetId ?? DEFAULT_ID; return useRGS(key, () => { const str = isServer ? null : localStorage.getItem(key); - return str ? { ...JSON.parse(str), systemColorScheme: media.matches ? DARK : LIGHT } : initialState; + return str ? { ...JSON.parse(str), systemColorScheme: matchMedia(MEDIA).matches ? DARK : LIGHT } : initialState; }); }; diff --git a/packages/shared-ui/src/globals.css b/packages/shared-ui/src/globals.css index a37611fc..50782132 100644 --- a/packages/shared-ui/src/globals.css +++ b/packages/shared-ui/src/globals.css @@ -1,3 +1,5 @@ +@import "nextjs-themes/src/styles.css"; + * { box-sizing: border-box; } From 91a056dc5f4cae938d0bed965509db875ec30f0a Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 22:55:27 +0530 Subject: [PATCH 13/17] clean up useTheme --- lib/nextjs-themes/src/hooks/use-theme.ts | 41 +++++++++--------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index 4a45f362..6c3c06ca 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -1,40 +1,31 @@ import useRGS, { SetStateAction } from "r18gs"; import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../constants"; import { resolveTheme } from "../utils"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; -const DELAY = 200; // ms - delay to allow reading from localStorage so that local storage does not override the new state -const map: Record = {}; -const createSetterWithFirstTimeDelay = (setThemeState: SetStateAction) => { - return (key: string) => { - if (map[key] === undefined) { - map[key] = 1; - return (arg: T) => setTimeout(() => setThemeState(state => ({ ...state, [key]: arg })), DELAY); - } else if (map[key] === 1) { - const fn = (arg: T) => setThemeState(state => ({ ...state, [key]: arg })); - map[key] = fn; - } - return map[key] as (arg: T) => void; - }; +/** create setter */ +const createSetter = (setThemeState: SetStateAction) => { + return (key: string) => + (arg: T) => + setThemeState(state => ({ ...state, [key]: arg })); }; +/** useTheme hook */ export const useTheme = (targetId?: string) => { const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); const { resolvedColorScheme, resolvedTheme } = resolveTheme(themeState); - const setterWithFirstTimeDelay = useMemo(() => createSetterWithFirstTimeDelay(setThemeState), []); + const setter = useMemo(() => createSetter(setThemeState), []); return { ...themeState, resolvedColorScheme, resolvedTheme, - setTheme: setterWithFirstTimeDelay("theme"), - setDarkTheme: setterWithFirstTimeDelay("darkTheme"), - setLightTheme: setterWithFirstTimeDelay("lightTheme"), - setThemeSet: useCallback( - (themeSet: { darkTheme: string; lightTheme: string }) => setThemeState(state => ({ ...state, ...themeSet })), - [], - ), - setColorSchemePref: setterWithFirstTimeDelay("colorSchemePref"), - setForcedTheme: setterWithFirstTimeDelay("forcedTheme"), - setForcedColorScheme: setterWithFirstTimeDelay("forcedColorScheme"), + setTheme: setter("theme"), + setDarkTheme: setter("darkTheme"), + setLightTheme: setter("lightTheme"), + setThemeSet: (themeSet: { darkTheme: string; lightTheme: string }) => + setThemeState(state => ({ ...state, ...themeSet })), + setColorSchemePref: setter("colorSchemePref"), + setForcedTheme: setter("forcedTheme"), + setForcedColorScheme: setter("forcedColorScheme"), }; }; From 853e396c82203b8b5b9ea4fb6830bbb8f0b7e2b9 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 22:55:44 +0530 Subject: [PATCH 14/17] deepsource fix --- .../server/nextjs/server-side-wrapper/server-side-wrapper.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx index 6b651089..f1947458 100644 --- a/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx +++ b/lib/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx @@ -20,6 +20,7 @@ export interface NextJsSSRThemeSwitcherProps extends HTMLProps { styles?: Record; } +/** getData props */ const getDataProps = (resolvedData: UpdateProps, styles?: Record) => { const dataProps: DataProps = { className: "" }; let classeNames = []; @@ -44,6 +45,8 @@ const getDataProps = (resolvedData: UpdateProps, styles?: Record return dataProps; }; +/** Shared server component renderer for Next.js SSG and SSR */ +/** @internal */ const sharedServerComponentRenderer = ( { children, tag, forcedPages, targetId, styles, ...props }: NextJsSSRThemeSwitcherProps, defaultTag: "div" | "html", From a036381be39ad8972554c02389dfabaf847af53e Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 23:01:10 +0530 Subject: [PATCH 15/17] patch use-theme test --- lib/nextjs-themes/src/hooks/use-theme.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 lib/nextjs-themes/src/hooks/use-theme.test.ts diff --git a/lib/nextjs-themes/src/hooks/use-theme.test.ts b/lib/nextjs-themes/src/hooks/use-theme.test.ts new file mode 100644 index 00000000..93015709 --- /dev/null +++ b/lib/nextjs-themes/src/hooks/use-theme.test.ts @@ -0,0 +1,11 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, test } from "vitest"; +import { useTheme } from "./use-theme"; + +describe("use-theme", () => { + test("setThemeSet", ({ expect }) => { + const hook = renderHook(() => useTheme()); + act(() => hook.result.current.setThemeSet({ darkTheme: "dark-1", lightTheme: "light-1" })); + expect(hook.result.current.darkTheme).toBe("dark-1"); + }); +}); From acff220d062e7961149f3f10ba8f58a9351f1b48 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 23:03:04 +0530 Subject: [PATCH 16/17] deepsource fix --- .../src/client/force-color-scheme/force-color-scheme.tsx | 2 +- lib/nextjs-themes/src/client/force-theme/force-theme.tsx | 2 +- lib/nextjs-themes/src/hooks/use-theme.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx index a218e33b..0d9514d8 100644 --- a/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx +++ b/lib/nextjs-themes/src/client/force-color-scheme/force-color-scheme.tsx @@ -1,9 +1,9 @@ "use client"; -import * as React from "react"; import { useEffect } from "react"; import type { ColorSchemeType } from "../../constants"; import { useTheme } from "../../hooks"; +/** Force color scheme on a page */ export const ForceColorScheme = (props: { colorScheme: ColorSchemeType }) => { const { setForcedColorScheme } = useTheme(); useEffect(() => { diff --git a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx index efff8f11..0b163b3e 100644 --- a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx +++ b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx @@ -1,8 +1,8 @@ "use client"; -import * as React from "react"; import { useEffect } from "react"; import { useTheme } from "../../hooks"; +/** Force theme on a page */ export const ForceTheme = (props: { theme: string }) => { const { setForcedTheme } = useTheme(); useEffect(() => { diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index 6c3c06ca..fe0b5adb 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -3,7 +3,7 @@ import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../co import { resolveTheme } from "../utils"; import { useMemo } from "react"; -/** create setter */ +/** create setter for memoized setter */ const createSetter = (setThemeState: SetStateAction) => { return (key: string) => (arg: T) => From d1f6181673861b47f60c1eea4ffba0762153d099 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 23:05:55 +0530 Subject: [PATCH 17/17] Apply changesets --- examples/app-router/CHANGELOG.md | 8 ++++++++ examples/app-router/package.json | 2 +- examples/pages-router/CHANGELOG.md | 8 ++++++++ examples/pages-router/package.json | 2 +- examples/simple-multi-theme/CHANGELOG.md | 8 ++++++++ examples/simple-multi-theme/package.json | 2 +- examples/tailwind/CHANGELOG.md | 7 +++++++ examples/tailwind/package.json | 2 +- examples/vite/CHANGELOG.md | 8 ++++++++ examples/vite/package.json | 2 +- lib/nextjs-themes/CHANGELOG.md | 6 ++++++ lib/nextjs-themes/package.json | 2 +- 12 files changed, 51 insertions(+), 6 deletions(-) diff --git a/examples/app-router/CHANGELOG.md b/examples/app-router/CHANGELOG.md index 62365f61..c9e95930 100644 --- a/examples/app-router/CHANGELOG.md +++ b/examples/app-router/CHANGELOG.md @@ -1,5 +1,13 @@ # app-router +## 0.0.22 + +### Patch Changes + +- Updated dependencies + - nextjs-themes@3.1.2 + - shared-ui@1.0.2 + ## 0.0.21 ### Patch Changes diff --git a/examples/app-router/package.json b/examples/app-router/package.json index 89e7b85c..c611f040 100644 --- a/examples/app-router/package.json +++ b/examples/app-router/package.json @@ -1,6 +1,6 @@ { "name": "app-router", - "version": "0.0.21", + "version": "0.0.22", "private": true, "scripts": { "dev": "next dev --port 3002", diff --git a/examples/pages-router/CHANGELOG.md b/examples/pages-router/CHANGELOG.md index 75532100..d10ace8a 100644 --- a/examples/pages-router/CHANGELOG.md +++ b/examples/pages-router/CHANGELOG.md @@ -1,5 +1,13 @@ # nextjs-pages-router +## 1.0.17 + +### Patch Changes + +- Updated dependencies + - nextjs-themes@3.1.2 + - shared-ui@1.0.2 + ## 1.0.16 ### Patch Changes diff --git a/examples/pages-router/package.json b/examples/pages-router/package.json index c769ff35..fb888bdf 100644 --- a/examples/pages-router/package.json +++ b/examples/pages-router/package.json @@ -1,6 +1,6 @@ { "name": "pages-router", - "version": "1.0.16", + "version": "1.0.17", "private": true, "scripts": { "dev": "next dev --port 3003", diff --git a/examples/simple-multi-theme/CHANGELOG.md b/examples/simple-multi-theme/CHANGELOG.md index 89ff543c..058f1201 100644 --- a/examples/simple-multi-theme/CHANGELOG.md +++ b/examples/simple-multi-theme/CHANGELOG.md @@ -1,5 +1,13 @@ # simple-multi-theme +## 1.0.17 + +### Patch Changes + +- Updated dependencies + - nextjs-themes@3.1.2 + - shared-ui@1.0.2 + ## 1.0.16 ### Patch Changes diff --git a/examples/simple-multi-theme/package.json b/examples/simple-multi-theme/package.json index 8dbd30c1..46e79c85 100644 --- a/examples/simple-multi-theme/package.json +++ b/examples/simple-multi-theme/package.json @@ -1,6 +1,6 @@ { "name": "simple-multi-theme", - "version": "1.0.16", + "version": "1.0.17", "private": true, "scripts": { "dev": "next dev --port 3001", diff --git a/examples/tailwind/CHANGELOG.md b/examples/tailwind/CHANGELOG.md index 59a368cf..790dacc6 100644 --- a/examples/tailwind/CHANGELOG.md +++ b/examples/tailwind/CHANGELOG.md @@ -1,5 +1,12 @@ # tailwind +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - nextjs-themes@3.1.2 + ## 0.1.10 ### Patch Changes diff --git a/examples/tailwind/package.json b/examples/tailwind/package.json index 0268c313..9c8c3886 100644 --- a/examples/tailwind/package.json +++ b/examples/tailwind/package.json @@ -1,6 +1,6 @@ { "name": "tailwind", - "version": "0.1.10", + "version": "0.1.11", "private": true, "scripts": { "dev": "next dev", diff --git a/examples/vite/CHANGELOG.md b/examples/vite/CHANGELOG.md index a84ef6b3..b3847fa4 100644 --- a/examples/vite/CHANGELOG.md +++ b/examples/vite/CHANGELOG.md @@ -1,5 +1,13 @@ # vite-example +## 0.0.22 + +### Patch Changes + +- Updated dependencies + - nextjs-themes@3.1.2 + - shared-ui@1.0.2 + ## 0.0.21 ### Patch Changes diff --git a/examples/vite/package.json b/examples/vite/package.json index a0b62de4..036afe28 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,7 +1,7 @@ { "name": "vite-example", "private": true, - "version": "0.0.21", + "version": "0.0.22", "type": "module", "scripts": { "dev": "vite --port 3001", diff --git a/lib/nextjs-themes/CHANGELOG.md b/lib/nextjs-themes/CHANGELOG.md index b3227426..63571200 100644 --- a/lib/nextjs-themes/CHANGELOG.md +++ b/lib/nextjs-themes/CHANGELOG.md @@ -1,5 +1,11 @@ # nextjs-themes +## 3.1.2 + +### Patch Changes + +- Fix FOUC + ## 3.1.1 ### Patch Changes diff --git a/lib/nextjs-themes/package.json b/lib/nextjs-themes/package.json index 9590779e..606f272d 100644 --- a/lib/nextjs-themes/package.json +++ b/lib/nextjs-themes/package.json @@ -2,7 +2,7 @@ "name": "nextjs-themes", "author": "Mayank Kumar Chaudhari ", "private": false, - "version": "3.1.1", + "version": "3.1.2", "description": "Unleash the Power of React Server Components! Use multiple themes on your site with confidence, without losing any advantages of React Server Components.", "main": "./index.ts", "types": "./index.ts",