From 40b2b914e052e67b9e979ae7408cc3f6fe5fe094 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sat, 24 Feb 2024 13:09:55 +0000 Subject: [PATCH 01/26] format --- .prettierignore | 3 +- .prettierrc | 3 +- lib/nextjs-themes/src/hooks/index.ts | 1 + lib/nextjs-themes/src/hooks/use-theme.ts | 1 + .../server-side-wrapper.tsx | 4 +- lib/nextjs-themes/turbo/generators/config.ts | 164 +++++++++--------- packages/shared-ui/src/root/description.tsx | 3 +- 7 files changed, 91 insertions(+), 88 deletions(-) create mode 100644 lib/nextjs-themes/src/hooks/index.ts create mode 100644 lib/nextjs-themes/src/hooks/use-theme.ts diff --git a/.prettierignore b/.prettierignore index 827d3cea..d7acc479 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -*lock.* \ No newline at end of file +*lock.* +docs diff --git a/.prettierrc b/.prettierrc index c2ff9e22..c61b6738 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "printWidth": 120, "tabWidth": 2, "arrowParens": "avoid", - "jsxBracketSameLine": true + "jsxBracketSameLine": true, + "bracketSameLine": true } diff --git a/lib/nextjs-themes/src/hooks/index.ts b/lib/nextjs-themes/src/hooks/index.ts new file mode 100644 index 00000000..767b68bf --- /dev/null +++ b/lib/nextjs-themes/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-theme"; diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts new file mode 100644 index 00000000..99ea5cf9 --- /dev/null +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -0,0 +1 @@ +export function useTheme() {} 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 0063ac66..0c3e19d5 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 @@ -24,8 +24,8 @@ function sharedServerComponentRenderer( const state = cookies().get("nextjs-themes")?.value; const path = headers().get("referer"); - const forcedPage = forcedPages?.find( - forcedPage => path?.match(Array.isArray(forcedPage) ? forcedPage[0] : forcedPage.pathMatcher), + const forcedPage = forcedPages?.find(forcedPage => + path?.match(Array.isArray(forcedPage) ? forcedPage[0] : forcedPage.pathMatcher), ); const forcedPageProps = Array.isArray(forcedPage) ? { forcedTheme: forcedPage[1].theme, forcedColorScheme: forcedPage[1].colorScheme } diff --git a/lib/nextjs-themes/turbo/generators/config.ts b/lib/nextjs-themes/turbo/generators/config.ts index 997d0661..f883cfc9 100644 --- a/lib/nextjs-themes/turbo/generators/config.ts +++ b/lib/nextjs-themes/turbo/generators/config.ts @@ -4,104 +4,104 @@ import type { PlopTypes } from "@turbo/gen"; // eslint-disable-next-line import/no-default-export -- export default is required for config files export default function generator(plop: PlopTypes.NodePlopAPI): void { - // A simple generator to add a new React component to the internal UI library - plop.setGenerator("react-component", { - description: "Adds a new react component", - prompts: [ - { - type: "input", - name: "name", - message: "What is the name of the component?", - }, - { - type: "confirm", - name: "isClient", - message: 'Is this a client component? (Should we add "use client" directive?)', - }, - { - type: "input", - name: "description", - message: "Describe your component. (This will be added as js-doc comment.)", - }, - ], - actions: data => (data ? getActions(data as InquirerDataType) : []), - }); + // A simple generator to add a new React component to the internal UI library + plop.setGenerator("react-component", { + description: "Adds a new react component", + prompts: [ + { + type: "input", + name: "name", + message: "What is the name of the component?", + }, + { + type: "confirm", + name: "isClient", + message: 'Is this a client component? (Should we add "use client" directive?)', + }, + { + type: "input", + name: "description", + message: "Describe your component. (This will be added as js-doc comment.)", + }, + ], + actions: data => (data ? getActions(data as InquirerDataType) : []), + }); } interface InquirerDataType { - isClient: boolean; - name: string; + isClient: boolean; + name: string; } function getActions(data: InquirerDataType) { - const { nestedRouteActions, root } = getNestedRouteActions(data); - return nestedRouteActions.concat([ - { - type: "add", - path: `${root}{{kebabCase name}}/index.ts`, - template: `${data.isClient ? '"use client";\n\n' : ""}export * from "./{{kebabCase name}}";\n`, - }, - { - type: "add", - path: `${root}{{kebabCase name}}/{{kebabCase name}}.tsx`, - templateFile: "templates/component.hbs", - }, - { - type: "add", - path: `${root}{{kebabCase name}}/{{kebabCase name}}.test.tsx`, - templateFile: "templates/component.test.hbs", - }, - { - type: "append", - path: `${root}index.ts`, - pattern: /(? component exports)/g, - template: 'export * from "./{{kebabCase name}}";', - }, - ]); + const { nestedRouteActions, root } = getNestedRouteActions(data); + return nestedRouteActions.concat([ + { + type: "add", + path: `${root}{{kebabCase name}}/index.ts`, + template: `${data.isClient ? '"use client";\n\n' : ""}export * from "./{{kebabCase name}}";\n`, + }, + { + type: "add", + path: `${root}{{kebabCase name}}/{{kebabCase name}}.tsx`, + templateFile: "templates/component.hbs", + }, + { + type: "add", + path: `${root}{{kebabCase name}}/{{kebabCase name}}.test.tsx`, + templateFile: "templates/component.test.hbs", + }, + { + type: "append", + path: `${root}index.ts`, + pattern: /(? component exports)/g, + template: 'export * from "./{{kebabCase name}}";', + }, + ]); } function getNestedRouteActions(data: InquirerDataType) { - const { isClient, name } = data; - const root = isClient ? "src/client/" : "src/server/"; - const nestedRouteActions: PlopTypes.ActionType[] = []; + const { isClient, name } = data; + const root = isClient ? "src/client/" : "src/server/"; + const nestedRouteActions: PlopTypes.ActionType[] = []; - /** Return early if no nested routes */ - if (!name.includes("/")) return { nestedRouteActions, root }; + /** Return early if no nested routes */ + if (!name.includes("/")) return { nestedRouteActions, root }; - const lastSlashInd = name.lastIndexOf("/") || name.lastIndexOf("\\"); - /** following is required to make sure appropreate name is used while creating components */ - data.name = name.slice(lastSlashInd + 1); + const lastSlashInd = name.lastIndexOf("/") || name.lastIndexOf("\\"); + /** following is required to make sure appropreate name is used while creating components */ + data.name = name.slice(lastSlashInd + 1); - const directories = name.slice(0, lastSlashInd).split(/\/|\\/); - const rootSegments = [...root.split(/\/|\\/)]; + const directories = name.slice(0, lastSlashInd).split(/\/|\\/); + const rootSegments = [...root.split(/\/|\\/)]; - for (let i = 1; i <= directories.length; i++) - updateIndexFilesIfNeeded(nestedRouteActions, rootSegments, directories.slice(0, i), isClient); + for (let i = 1; i <= directories.length; i++) + updateIndexFilesIfNeeded(nestedRouteActions, rootSegments, directories.slice(0, i), isClient); - return { nestedRouteActions, root: `${root + directories.join("/")}/` }; + return { nestedRouteActions, root: `${root + directories.join("/")}/` }; } function updateIndexFilesIfNeeded( - nestedRouteActions: PlopTypes.ActionType[], - rootSegments: string[], - currentDirSegments: string[], - isClient: boolean, + nestedRouteActions: PlopTypes.ActionType[], + rootSegments: string[], + currentDirSegments: string[], + isClient: boolean, ) { - const indexFilePath = path.resolve(process.cwd(), "..", "..", ...rootSegments, ...currentDirSegments, "index.ts"); - const root = rootSegments.join("/"); - if (!fs.existsSync(indexFilePath)) { - const content = `${isClient ? '"use client";\n' : ""}// ${currentDirSegments.join("/")} component exports\n`; - nestedRouteActions.push({ - type: "add", - path: `${root + currentDirSegments.join("/")}/index.ts`, - template: content, - }); - const length = currentDirSegments.length; - nestedRouteActions.push({ - type: "append", - pattern: /(? component exports)/g, - path: `${root + (length === 1 ? "" : `${currentDirSegments.slice(0, length - 1).join("/")}/`)}index.ts`, - template: `export * from "./${currentDirSegments[length - 1]}"`, - }); - } + const indexFilePath = path.resolve(process.cwd(), "..", "..", ...rootSegments, ...currentDirSegments, "index.ts"); + const root = rootSegments.join("/"); + if (!fs.existsSync(indexFilePath)) { + const content = `${isClient ? '"use client";\n' : ""}// ${currentDirSegments.join("/")} component exports\n`; + nestedRouteActions.push({ + type: "add", + path: `${root + currentDirSegments.join("/")}/index.ts`, + template: content, + }); + const length = currentDirSegments.length; + nestedRouteActions.push({ + type: "append", + pattern: /(? component exports)/g, + path: `${root + (length === 1 ? "" : `${currentDirSegments.slice(0, length - 1).join("/")}/`)}index.ts`, + template: `export * from "./${currentDirSegments[length - 1]}"`, + }); + } } diff --git a/packages/shared-ui/src/root/description.tsx b/packages/shared-ui/src/root/description.tsx index 6f4c4767..9925f272 100644 --- a/packages/shared-ui/src/root/description.tsx +++ b/packages/shared-ui/src/root/description.tsx @@ -9,8 +9,7 @@ export function Description() { className={styles.logo} href="https://github.com/react18-tools/nextjs-themes" rel="noopener noreferrer" - target="_blank" - > + target="_blank">

From b80da1ac64144b36742b540bf113ab394149d472 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sat, 24 Feb 2024 16:57:46 +0000 Subject: [PATCH 02/26] add r18gs --- lib/nextjs-themes/package.json | 1 + lib/nextjs-themes/src/constants.ts | 34 +++++++++++++++++++++ lib/nextjs-themes/src/hooks/use-theme.ts | 7 ++++- packages/shared-ui/src/root/description.tsx | 1 - 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 lib/nextjs-themes/src/constants.ts diff --git a/lib/nextjs-themes/package.json b/lib/nextjs-themes/package.json index 1321d885..b01247de 100644 --- a/lib/nextjs-themes/package.json +++ b/lib/nextjs-themes/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "persist-and-sync": "^1.2.1", + "r18gs": "^0.0.3", "zustand": "^4.5.1" }, "peerDependencies": { diff --git a/lib/nextjs-themes/src/constants.ts b/lib/nextjs-themes/src/constants.ts new file mode 100644 index 00000000..c3664979 --- /dev/null +++ b/lib/nextjs-themes/src/constants.ts @@ -0,0 +1,34 @@ +/** shared constants -- keep in separate files for better tree-shaking and dependency injection */ +export const DEFAULT_ID = "nth"; + +export type ColorSchemeType = "" | "system" | "dark" | "light"; + +export interface ThemeStoreType { + theme: string; + darkTheme: string; + lightTheme: string; + colorSchemePref: ColorSchemeType; + resolvedTheme: string; + resolvedColorScheme: "dark" | "light"; + forcedTheme?: string; + forcedColorScheme?: ColorSchemeType; +} + +export type ThemeStoreActionsType = { + setTheme: (theme: string) => void; + setDarkTheme: (darkTheme: string) => void; + setLightTheme: (lightTheme: string) => void; + setThemeSet: (themeSet: { darkTheme: string; lightTheme: string }) => void; + setColorSchemePref: (colorSchemePref: ColorSchemeType) => void; + setForcedTheme: (forcedTheme?: string) => void; + setForcedColorScheme: (forcedColorScheme?: ColorSchemeType) => void; +}; + +export const initialState: ThemeStoreType = { + theme: "", + resolvedTheme: "", + darkTheme: "dark", + lightTheme: "", + colorSchemePref: "system", + resolvedColorScheme: "light", +}; diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index 99ea5cf9..010d6477 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -1 +1,6 @@ -export function useTheme() {} +import useRGS from "r18gs"; +import { DEFAULT_ID, ThemeStoreType, initialState } from "../constants"; + +export function useTheme(targetId?: string) { + const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); +} \ No newline at end of file diff --git a/packages/shared-ui/src/root/description.tsx b/packages/shared-ui/src/root/description.tsx index 9925f272..2bacac94 100644 --- a/packages/shared-ui/src/root/description.tsx +++ b/packages/shared-ui/src/root/description.tsx @@ -1,4 +1,3 @@ -import type { HTMLProps } from "react"; import { Logo } from "../common/logo"; import styles from "../root-layout.module.css"; From 823043b90a04cd26fc4e2365afaef3dee438eadc Mon Sep 17 00:00:00 2001 From: Mayank Date: Sat, 24 Feb 2024 17:05:44 +0000 Subject: [PATCH 03/26] useTheme hook for the library users --- lib/nextjs-themes/src/hooks/use-theme.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index 010d6477..ad4231e5 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -1,6 +1,18 @@ import useRGS from "r18gs"; -import { DEFAULT_ID, ThemeStoreType, initialState } from "../constants"; +import { ColorSchemeType, DEFAULT_ID, ThemeStoreActionsType, ThemeStoreType, initialState } from "../constants"; -export function useTheme(targetId?: string) { +export function useTheme(targetId?: string): ThemeStoreType & ThemeStoreActionsType { const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); -} \ No newline at end of file + return { + ...themeState, + setTheme: (theme: string) => setThemeState(state => ({ ...state, theme })), + setDarkTheme: (darkTheme: string) => setThemeState(state => ({ ...state, darkTheme })), + setLightTheme: (lightTheme: string) => setThemeState(state => ({ ...state, lightTheme })), + setThemeSet: (themeSet: { darkTheme: string; lightTheme: string }) => + setThemeState(state => ({ ...state, ...themeSet })), + setColorSchemePref: (colorSchemePref: ColorSchemeType) => setThemeState(state => ({ ...state, colorSchemePref })), + setForcedTheme: (forcedTheme?: string) => setThemeState(state => ({ ...state, forcedTheme })), + setForcedColorScheme: (forcedColorScheme?: ColorSchemeType) => + setThemeState(state => ({ ...state, forcedColorScheme })), + }; +} From 72a5deda90d821042def556be3d4bfcaf8800f4d Mon Sep 17 00:00:00 2001 From: Mayank Date: Sat, 24 Feb 2024 17:41:57 +0000 Subject: [PATCH 04/26] update theme-switcher --- .../client/theme-switcher/theme-switcher.tsx | 66 ++++++++----------- lib/nextjs-themes/src/constants.ts | 2 + lib/nextjs-themes/src/styles.css | 16 +++++ lib/nextjs-themes/src/utils.ts | 6 +- 4 files changed, 50 insertions(+), 40 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 c65c527b..f8677b55 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -1,20 +1,29 @@ import * as React from "react"; import { useEffect } from "react"; -import type { ColorSchemeType } from "../../store"; -import { useTheme } from "../../store"; import { resolveTheme } from "../../utils"; -import { StorageType } from "persist-and-sync"; +import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../../constants"; +import useRGS, { SetStateAction } from "r18gs"; export interface ThemeSwitcherProps { forcedTheme?: string; forcedColorScheme?: ColorSchemeType; targetSelector?: string; themeTransition?: string; - /** - * defaultValue `"cookies"` - * set storage to `localStorage` or `sessionsStorage` when using only client side or when you must avoid using cookies - */ - storage?: StorageType; +} + +function 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]); } export interface DataProps { @@ -53,7 +62,8 @@ const updateDOM = ( const disableAnimation = (themeTransition = "none") => { const css = document.createElement("style"); - const transition = `transition: ${themeTransition} !important;`; + /** 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}}`), ); @@ -74,39 +84,19 @@ 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 function useThemeSwitcher(props: ThemeSwitcherProps) { - const [setStorage, ...depArray] = useTheme(state => [ - state.setStorage, - state.theme, - state.darkTheme, - state.lightTheme, - state.colorSchemePref, - state.forcedColorScheme, - state.forcedTheme, - ]); + const [themeState, setThemeState] = useRGS(props.targetSelector ?? DEFAULT_ID, initialState); + useMediaQuery(setThemeState); useEffect(() => { - setStorage(props.storage ?? "cookies"); - }, [props.storage]); + const restoreTransitions = disableAnimation(props.themeTransition); - useEffect(() => { - const themeState = useTheme.getState(); - const media = matchMedia("(prefers-color-scheme: dark)"); - const updateTheme = () => { - const restoreTransitions = disableAnimation(props.themeTransition); - - const resolvedData = resolveTheme(media.matches, themeState, props); - themeState.setResolved(resolvedData); - updateDOM(resolvedData, media.matches, props.targetSelector); - - restoreTransitions(); - }; + const resolvedData = resolveTheme(themeState, props); + const { resolvedColorScheme, resolvedTheme } = resolvedData; + setThemeState(state => ({ ...state, resolvedColorScheme, resolvedTheme })); + updateDOM(resolvedData, themeState.systemColorScheme === "dark", props.targetSelector); - media.addEventListener("change", updateTheme); - updateTheme(); - return () => { - media.removeEventListener("change", updateTheme); - }; - }, [props.forcedColorScheme, props.forcedTheme, props.targetSelector, ...depArray]); + restoreTransitions(); + }, [props, themeState]); } /** diff --git a/lib/nextjs-themes/src/constants.ts b/lib/nextjs-themes/src/constants.ts index c3664979..9c7496d2 100644 --- a/lib/nextjs-themes/src/constants.ts +++ b/lib/nextjs-themes/src/constants.ts @@ -10,6 +10,7 @@ export interface ThemeStoreType { colorSchemePref: ColorSchemeType; resolvedTheme: string; resolvedColorScheme: "dark" | "light"; + systemColorScheme: "dark" | "light"; forcedTheme?: string; forcedColorScheme?: ColorSchemeType; } @@ -30,5 +31,6 @@ export const initialState: ThemeStoreType = { darkTheme: "dark", lightTheme: "", colorSchemePref: "system", + systemColorScheme: "light", resolvedColorScheme: "light", }; diff --git a/lib/nextjs-themes/src/styles.css b/lib/nextjs-themes/src/styles.css index d3c06f95..f1f150d6 100644 --- a/lib/nextjs-themes/src/styles.css +++ b/lib/nextjs-themes/src/styles.css @@ -34,6 +34,7 @@ box-shadow: calc(var(--size) / 4) calc(var(--size) / -4) calc(var(--size) / 8) inset #fff; border: none; background: transparent; + animation: swing linear 0.5s; } [data-csp="light"] .nextjs-themes--color-switch, @@ -43,3 +44,18 @@ background-color: yellow; border: 1px solid orangered; } + +@keyframes swing { + 40% { + transform: rotate(-15deg); + } + + 80% { + transform: rotate(10deg); + } + + 0%, + 100% { + transform: rotate(0deg); + } +} diff --git a/lib/nextjs-themes/src/utils.ts b/lib/nextjs-themes/src/utils.ts index 8800be6f..6369fa5f 100644 --- a/lib/nextjs-themes/src/utils.ts +++ b/lib/nextjs-themes/src/utils.ts @@ -1,13 +1,15 @@ import { ThemeSwitcherProps, UpdateProps } from "./client"; -import type { ThemeStoreType } from "./store"; +import { ThemeStoreType } from "./constants"; -export function resolveTheme(isSystemDark: boolean, state?: ThemeStoreType, props?: ThemeSwitcherProps): UpdateProps { +export function 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 isSystemDark = state?.systemColorScheme === "dark"; + let resolvedColorScheme: "dark" | "light" = isSystemDark ? "dark" : "light"; let resolvedTheme = resolvedForcedTheme === undefined ? state?.theme || "" : resolvedForcedTheme; From 60964dfd3d844367a9424a5810c3303ac668dbed Mon Sep 17 00:00:00 2001 From: Mayank Date: Sat, 24 Feb 2024 17:52:58 +0000 Subject: [PATCH 05/26] tkb --- .tkb | 29 +++++++++++++++++++++++++++++ .vscode/settings.json | 3 ++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .tkb diff --git a/.tkb b/.tkb new file mode 100644 index 00000000..e1c4758a --- /dev/null +++ b/.tkb @@ -0,0 +1,29 @@ +{ + "scope": "Workspace", + "tasks": { + "task-hlCkRvoudoMFloe9AQamt": { + "id": "task-hlCkRvoudoMFloe9AQamt", + "description": "Add persistence and cookies\n", + "columnId": "column-todo" + } + }, + "columns": [ + { + "id": "column-todo", + "title": "To do", + "tasksIds": [ + "task-hlCkRvoudoMFloe9AQamt" + ] + }, + { + "id": "column-doing", + "title": "Doing", + "tasksIds": [] + }, + { + "id": "column-done", + "title": "Done", + "tasksIds": [] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index fe5bd20a..a782489e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "editor.formatOnSave": true, - "editor.formatOnPaste": true + "editor.formatOnPaste": true, + "mayank1513.trello-kanban.Workspace.filePath": ".tkb" } \ No newline at end of file From 8212e9ef981f8aa0c36dcc04425ce98abd9937d1 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sat, 24 Feb 2024 17:53:38 +0000 Subject: [PATCH 06/26] Add r18gs and remove zustand --- lib/nextjs-themes/__mocks__/zustand.ts | 47 -------------- lib/nextjs-themes/lite.js | 2 +- lib/nextjs-themes/package.json | 4 +- .../src/client/color-switch/color-switch.tsx | 4 +- .../force-color-scheme/force-color-scheme.tsx | 6 +- .../src/client/force-theme/force-theme.tsx | 4 +- .../client/theme-switcher/theme-switcher.tsx | 1 + lib/nextjs-themes/src/index.ts | 2 +- .../server-side-wrapper.tsx | 4 +- lib/nextjs-themes/src/store.ts | 62 ------------------- 10 files changed, 13 insertions(+), 123 deletions(-) delete mode 100644 lib/nextjs-themes/__mocks__/zustand.ts delete mode 100644 lib/nextjs-themes/src/store.ts diff --git a/lib/nextjs-themes/__mocks__/zustand.ts b/lib/nextjs-themes/__mocks__/zustand.ts deleted file mode 100644 index 9db14ec3..00000000 --- a/lib/nextjs-themes/__mocks__/zustand.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type * as zustand from "zustand"; -import { act } from "@testing-library/react"; -import { afterEach, vi } from "vitest"; - -const { create: actualCreate, createStore: actualCreateStore } = await vi.importActual("zustand"); - -// a variable to hold reset functions for all stores declared in the app -export const storeResetFns = new Set<() => void>(); - -const createUncurried = (stateCreator: zustand.StateCreator) => { - const store = actualCreate(stateCreator); - const initialState = store.getState(); - storeResetFns.add(() => { - store.setState(initialState, true); - }); - return store; -}; - -// when creating a store, we get its initial state, create a reset function and add it in the set -export const create = ((stateCreator: zustand.StateCreator) => { - // to support curried version of create - return typeof stateCreator === "function" ? createUncurried(stateCreator) : createUncurried; -}) as typeof zustand.create; - -const createStoreUncurried = (stateCreator: zustand.StateCreator) => { - const store = actualCreateStore(stateCreator); - const initialState = store.getState(); - storeResetFns.add(() => { - store.setState(initialState, true); - }); - return store; -}; - -// when creating a store, we get its initial state, create a reset function and add it in the set -export const createStore = ((stateCreator: zustand.StateCreator) => { - // to support curried version of createStore - return typeof stateCreator === "function" ? createStoreUncurried(stateCreator) : createStoreUncurried; -}) as typeof zustand.createStore; - -// reset all stores after each test run -afterEach(() => { - act(() => { - storeResetFns.forEach(resetFn => { - resetFn(); - }); - }); -}); diff --git a/lib/nextjs-themes/lite.js b/lib/nextjs-themes/lite.js index b6bae508..b4d045d1 100644 --- a/lib/nextjs-themes/lite.js +++ b/lib/nextjs-themes/lite.js @@ -6,7 +6,7 @@ const path = require("node:path"); const packageJson = require(path.resolve(__dirname, "package.json")); const ref = packageJson.name; -packageJson.peerDependencies.zustand = "^3 || ^4"; +packageJson.peerDependencies.r18gs = "^0"; packageJson.name = `${packageJson.name}-lite`; fs.writeFileSync(path.resolve(__dirname, "package.json"), JSON.stringify(packageJson, null, 2)); diff --git a/lib/nextjs-themes/package.json b/lib/nextjs-themes/package.json index b01247de..8eb8197b 100644 --- a/lib/nextjs-themes/package.json +++ b/lib/nextjs-themes/package.json @@ -48,9 +48,7 @@ "vitest": "^1.3.0" }, "dependencies": { - "persist-and-sync": "^1.2.1", - "r18gs": "^0.0.3", - "zustand": "^4.5.1" + "r18gs": "^0.0.3" }, "peerDependencies": { "@types/react": "16.8 - 18", 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 cddec46b..9471bf4b 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,5 @@ import * as React from "react"; -import { useTheme } from "../../store"; +import { useTheme } from "../../hooks"; export interface ColorSwitchProps { /** Diameter of the color switch */ @@ -23,7 +23,7 @@ export interface ColorSwitchProps { * ``` */ export function ColorSwitch({ size = 25, skipSystem }: ColorSwitchProps) { - const [colorSchemePref, setColorSchemePref] = useTheme(state => [state.colorSchemePref, state.setColorSchemePref]); + const { colorSchemePref, setColorSchemePref } = useTheme(); const toggleColorScheme = () => { switch (colorSchemePref) { case "": 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 fad2c778..c223b3ad 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,11 +1,11 @@ "use client"; import * as React from "react"; import { useEffect } from "react"; -import type { ColorSchemeType } from "../../store"; -import { useTheme } from "../../store"; +import type { ColorSchemeType } from "../../constants"; +import { useTheme } from "../../hooks"; export function ForceColorScheme(props: { colorScheme: ColorSchemeType }) { - const [setForcedColorScheme] = useTheme(state => [state.setForcedColorScheme]); + const { setForcedColorScheme } = useTheme(); useEffect(() => { setForcedColorScheme(props.colorScheme); return () => { 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 892b4c30..69ea7f14 100644 --- a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx +++ b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx @@ -1,10 +1,10 @@ "use client"; import * as React from "react"; import { useEffect } from "react"; -import { useTheme } from "../../store"; +import { useTheme } from "../../hooks"; export function ForceTheme(props: { theme: string }) { - const [setForcedTheme] = useTheme(state => [state.setForcedTheme]); + const { setForcedTheme } = useTheme(); useEffect(() => { setForcedTheme(props.theme); return () => { 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 f8677b55..ce99f46e 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -4,6 +4,7 @@ import { resolveTheme } from "../../utils"; import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../../constants"; import useRGS, { SetStateAction } from "r18gs"; +/** todo - set persistance and cookies */ export interface ThemeSwitcherProps { forcedTheme?: string; forcedColorScheme?: ColorSchemeType; diff --git a/lib/nextjs-themes/src/index.ts b/lib/nextjs-themes/src/index.ts index 10a110c9..4f7fe5b0 100644 --- a/lib/nextjs-themes/src/index.ts +++ b/lib/nextjs-themes/src/index.ts @@ -1,4 +1,4 @@ "use client"; // client component exports export * from "./client"; -export * from "./store"; +export * from "./hooks"; 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 0c3e19d5..984f124b 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 @@ -1,7 +1,7 @@ import * as React from "react"; import type { HTMLProps, ReactNode } from "react"; import { cookies, headers } from "next/headers"; -import type { ColorSchemeType, ThemeStoreType } from "../../../store"; +import type { ColorSchemeType, ThemeStoreType } from "../../../constants"; import { resolveTheme } from "../../../utils"; import { DataProps, UpdateProps } from "../../../client"; @@ -32,7 +32,7 @@ function sharedServerComponentRenderer( : forcedPage?.props; const themeState = state ? (JSON.parse(state) as ThemeStoreType) : undefined; const isSystemDark = cookies().get("data-color-scheme-system")?.value === "dark"; - const resolvedData = resolveTheme(isSystemDark, themeState, forcedPageProps); + const resolvedData = resolveTheme(themeState, forcedPageProps); const dataProps = getDataProps(resolvedData); return ( diff --git a/lib/nextjs-themes/src/store.ts b/lib/nextjs-themes/src/store.ts deleted file mode 100644 index 0390eecd..00000000 --- a/lib/nextjs-themes/src/store.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { create } from "zustand"; -import { PersistNSyncOptionsType, StorageType, persistNSync } from "persist-and-sync"; - -export type ColorSchemeType = "" | "system" | "dark" | "light"; - -export type ThemeStoreType = { - theme: string; - darkTheme: string; - lightTheme: string; - colorSchemePref: ColorSchemeType; - resolvedTheme: string; - resolvedColorScheme: ColorSchemeType; - forcedTheme?: string; - forcedColorScheme?: ColorSchemeType; - __persistNSyncOptions: PersistNSyncOptionsType; -}; - -export type ThemeStoreActionsType = { - setTheme: (theme: string) => void; - setDarkTheme: (darkTheme: string) => void; - setLightTheme: (lightTheme: string) => void; - setThemeSet: (themeSet: { darkTheme: string; lightTheme: string }) => void; - setColorSchemePref: (colorSchemePref: ColorSchemeType) => void; - setForcedTheme: (forcedTheme?: string) => void; - setForcedColorScheme: (forcedColorScheme?: ColorSchemeType) => void; - setResolved: (resolved: { resolvedTheme: string; resolvedColorScheme: ColorSchemeType }) => void; - setStorage: (storage: StorageType) => void; -}; - -const storeOptions: PersistNSyncOptionsType = { - name: "nextjs-themes", - exclude: [/forced/, /resolved/, /^__/], - storage: "cookies", -}; - -export const initialState: ThemeStoreType = { - theme: "", - resolvedTheme: "", - darkTheme: "dark", - lightTheme: "", - colorSchemePref: "system", - resolvedColorScheme: "system", - __persistNSyncOptions: storeOptions, -}; - -export const useTheme = create()( - persistNSync( - (set, get) => ({ - ...initialState, - setTheme: theme => set({ ...get(), theme }), - setDarkTheme: darkTheme => set({ ...get(), darkTheme }), - setLightTheme: lightTheme => set({ ...get(), lightTheme }), - setForcedTheme: forcedTheme => set({ ...get(), forcedTheme }), - setForcedColorScheme: forcedColorScheme => set({ ...get(), forcedColorScheme }), - setColorSchemePref: colorSchemePref => set({ ...get(), colorSchemePref }), - setResolved: ({ resolvedColorScheme, resolvedTheme }) => set({ ...get(), resolvedColorScheme, resolvedTheme }), - setThemeSet: ({ lightTheme, darkTheme }) => set({ ...get(), lightTheme, darkTheme }), - setStorage: storage => set({ ...get(), __persistNSyncOptions: { ...storeOptions, storage } }), - }), - storeOptions, - ), -); From de5c9e5e666df95efdffe926814ef0c42134b748 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 04:10:49 +0000 Subject: [PATCH 07/26] fix useEffect infinite loop --- .../src/client/theme-switcher/theme-switcher.tsx | 2 -- lib/nextjs-themes/src/constants.ts | 4 ---- lib/nextjs-themes/src/hooks/use-theme.ts | 8 ++++++-- 3 files changed, 6 insertions(+), 8 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 ce99f46e..4a73a928 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -92,8 +92,6 @@ export function useThemeSwitcher(props: ThemeSwitcherProps) { const restoreTransitions = disableAnimation(props.themeTransition); const resolvedData = resolveTheme(themeState, props); - const { resolvedColorScheme, resolvedTheme } = resolvedData; - setThemeState(state => ({ ...state, resolvedColorScheme, resolvedTheme })); updateDOM(resolvedData, themeState.systemColorScheme === "dark", props.targetSelector); restoreTransitions(); diff --git a/lib/nextjs-themes/src/constants.ts b/lib/nextjs-themes/src/constants.ts index 9c7496d2..136769c2 100644 --- a/lib/nextjs-themes/src/constants.ts +++ b/lib/nextjs-themes/src/constants.ts @@ -8,8 +8,6 @@ export interface ThemeStoreType { darkTheme: string; lightTheme: string; colorSchemePref: ColorSchemeType; - resolvedTheme: string; - resolvedColorScheme: "dark" | "light"; systemColorScheme: "dark" | "light"; forcedTheme?: string; forcedColorScheme?: ColorSchemeType; @@ -27,10 +25,8 @@ export type ThemeStoreActionsType = { export const initialState: ThemeStoreType = { theme: "", - resolvedTheme: "", darkTheme: "dark", lightTheme: "", colorSchemePref: "system", systemColorScheme: "light", - resolvedColorScheme: "light", }; diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index ad4231e5..14a34fe3 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -1,10 +1,14 @@ import useRGS from "r18gs"; -import { ColorSchemeType, DEFAULT_ID, ThemeStoreActionsType, ThemeStoreType, initialState } from "../constants"; +import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../constants"; +import { resolveTheme } from "../utils"; -export function useTheme(targetId?: string): ThemeStoreType & ThemeStoreActionsType { +export function useTheme(targetId?: string) { const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); + const { resolvedColorScheme, resolvedTheme } = resolveTheme(themeState); return { ...themeState, + resolvedColorScheme, + resolvedTheme, setTheme: (theme: string) => setThemeState(state => ({ ...state, theme })), setDarkTheme: (darkTheme: string) => setThemeState(state => ({ ...state, darkTheme })), setLightTheme: (lightTheme: string) => setThemeState(state => ({ ...state, lightTheme })), From 657072754674b66515d4309bfe4895e4f1e41078 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 04:15:01 +0000 Subject: [PATCH 08/26] fix hook import --- lib/nextjs-themes/src/client/color-switch/color-switch.test.tsx | 2 +- .../src/client/force-color-scheme/force-color-scheme.test.tsx | 2 +- lib/nextjs-themes/src/client/force-theme/force-theme.test.tsx | 2 +- .../src/client/theme-switcher/theme-switcher.test.tsx | 2 +- 4 files changed, 4 insertions(+), 4 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 4298e3c4..ad553107 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 @@ -1,6 +1,6 @@ import { act, cleanup, fireEvent, render, renderHook, screen } from "@testing-library/react"; import { ColorSwitch } from "./color-switch"; -import { useTheme } from "../../store"; +import { useTheme } from "../../hooks"; describe("color-switch", () => { afterEach(cleanup); 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 ef3be6b1..d6fefe65 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 @@ -1,6 +1,6 @@ import { act, cleanup, render, renderHook } from "@testing-library/react"; import { afterEach, describe, test } from "vitest"; -import { useTheme } from "../../store"; +import { useTheme } from "../../hooks"; import { ForceColorScheme } from "./force-color-scheme"; describe.concurrent("force-color-scheme", () => { diff --git a/lib/nextjs-themes/src/client/force-theme/force-theme.test.tsx b/lib/nextjs-themes/src/client/force-theme/force-theme.test.tsx index e9e729d2..1520a0a2 100644 --- a/lib/nextjs-themes/src/client/force-theme/force-theme.test.tsx +++ b/lib/nextjs-themes/src/client/force-theme/force-theme.test.tsx @@ -1,6 +1,6 @@ import { act, cleanup, render, renderHook } from "@testing-library/react"; import { afterEach, describe, test } from "vitest"; -import { useTheme } from "../../store"; +import { useTheme } from "../../hooks"; import { ForceTheme } from "./force-theme"; describe.concurrent("force-color-scheme", () => { 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 006f176d..124152e1 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 @@ -1,6 +1,6 @@ import { act, cleanup, render, renderHook } from "@testing-library/react"; import { afterEach, describe, test } from "vitest"; -import { useTheme } from "../../store"; +import { useTheme } from "../../hooks"; import { ThemeSwitcher } from "./theme-switcher"; import { getResolvedColorScheme, getResolvedTheme } from "../../utils"; From 0871b2a916ce3d17c06053a7dfeffde561949e8a Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 04:25:14 +0000 Subject: [PATCH 09/26] fix infinite loops because of recreating setter functions --- .../force-color-scheme/force-color-scheme.tsx | 2 +- lib/nextjs-themes/src/hooks/use-theme.ts | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 10 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 c223b3ad..5aa2e51e 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 @@ -11,6 +11,6 @@ export function ForceColorScheme(props: { colorScheme: ColorSchemeType }) { return () => { setForcedColorScheme(undefined); }; - }, [props.colorScheme, setForcedColorScheme]); + }, [props.colorScheme]); return null; } diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index 14a34fe3..b45c6eb2 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -1,6 +1,7 @@ import useRGS from "r18gs"; import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../constants"; import { resolveTheme } from "../utils"; +import { useCallback } from "react"; export function useTheme(targetId?: string) { const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); @@ -9,14 +10,21 @@ export function useTheme(targetId?: string) { ...themeState, resolvedColorScheme, resolvedTheme, - setTheme: (theme: string) => setThemeState(state => ({ ...state, theme })), - setDarkTheme: (darkTheme: string) => setThemeState(state => ({ ...state, darkTheme })), - setLightTheme: (lightTheme: string) => setThemeState(state => ({ ...state, lightTheme })), - setThemeSet: (themeSet: { darkTheme: string; lightTheme: string }) => - setThemeState(state => ({ ...state, ...themeSet })), - setColorSchemePref: (colorSchemePref: ColorSchemeType) => setThemeState(state => ({ ...state, colorSchemePref })), - setForcedTheme: (forcedTheme?: string) => setThemeState(state => ({ ...state, forcedTheme })), - setForcedColorScheme: (forcedColorScheme?: ColorSchemeType) => - setThemeState(state => ({ ...state, forcedColorScheme })), + setTheme: useCallback((theme: string) => setThemeState(state => ({ ...state, theme })), []), + setDarkTheme: useCallback((darkTheme: string) => setThemeState(state => ({ ...state, darkTheme })), []), + setLightTheme: useCallback((lightTheme: string) => setThemeState(state => ({ ...state, lightTheme })), []), + setThemeSet: useCallback( + (themeSet: { darkTheme: string; lightTheme: string }) => setThemeState(state => ({ ...state, ...themeSet })), + [], + ), + setColorSchemePref: useCallback( + (colorSchemePref: ColorSchemeType) => setThemeState(state => ({ ...state, colorSchemePref })), + [], + ), + setForcedTheme: useCallback((forcedTheme?: string) => setThemeState(state => ({ ...state, forcedTheme })), []), + setForcedColorScheme: useCallback( + (forcedColorScheme?: ColorSchemeType) => setThemeState(state => ({ ...state, forcedColorScheme })), + [], + ), }; } From e2ac42041ec6034c259ef2712a6b67c95b8d9c25 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 05:26:03 +0000 Subject: [PATCH 10/26] fix tests setup and tests --- lib/nextjs-themes/__mocks__/vitest-env.d.ts | 2 -- .../src/client/force-theme/force-theme.tsx | 2 +- .../src/client/theme-switcher/theme-switcher.test.tsx | 6 +++++- .../server-side-wrapper/server-side-wrapper.test.tsx | 2 +- lib/nextjs-themes/vitest.setup.ts | 11 +++++++++-- 5 files changed, 16 insertions(+), 7 deletions(-) delete mode 100644 lib/nextjs-themes/__mocks__/vitest-env.d.ts diff --git a/lib/nextjs-themes/__mocks__/vitest-env.d.ts b/lib/nextjs-themes/__mocks__/vitest-env.d.ts deleted file mode 100644 index 97ce4524..00000000 --- a/lib/nextjs-themes/__mocks__/vitest-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// 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 69ea7f14..da10db80 100644 --- a/lib/nextjs-themes/src/client/force-theme/force-theme.tsx +++ b/lib/nextjs-themes/src/client/force-theme/force-theme.tsx @@ -10,6 +10,6 @@ export function ForceTheme(props: { theme: string }) { return () => { setForcedTheme(undefined); }; - }, [props.theme, setForcedTheme]); + }, [props.theme]); return null; } 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 124152e1..72ebb0ce 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 @@ -3,12 +3,16 @@ import { afterEach, describe, test } from "vitest"; import { useTheme } from "../../hooks"; import { ThemeSwitcher } from "./theme-switcher"; import { getResolvedColorScheme, getResolvedTheme } from "../../utils"; +import useRGS from "r18gs"; +import { DEFAULT_ID, initialState } from "../../constants"; /** * -> concurrency is not feasible because of global store conflicts */ describe("theme-switcher", () => { - afterEach(cleanup); + afterEach(() => { + cleanup(); + }); test("Test defaultDark and defaultLight themes", async ({ expect }) => { const { result } = renderHook(() => useTheme()); 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 79a0a21a..0576fa27 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 @@ -1,5 +1,5 @@ import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, test } from "vitest"; +import { afterEach, describe, test, beforeEach } from "vitest"; import { NextJsSSGThemeSwitcher, ServerSideWrapper } from "."; describe("server-side-target", () => { diff --git a/lib/nextjs-themes/vitest.setup.ts b/lib/nextjs-themes/vitest.setup.ts index 66e8293e..2096517b 100644 --- a/lib/nextjs-themes/vitest.setup.ts +++ b/lib/nextjs-themes/vitest.setup.ts @@ -1,4 +1,7 @@ -import { vi } from "vitest"; +import useRGS from "r18gs"; +import { vi, beforeEach } from "vitest"; +import { DEFAULT_ID, initialState } from "./src/constants"; +import { act, renderHook } from "@testing-library/react"; // mock matchMedia Object.defineProperty(window, "matchMedia", { @@ -33,4 +36,8 @@ vi.mock("next/headers", () => ({ headers: () => ({ get: (h: string) => globalThis.path }), })); -vi.mock("zustand"); +/** reset global state */ +beforeEach(() => { + const { result } = renderHook(() => useRGS(DEFAULT_ID)); + act(() => result.current[1](initialState)); +}); From 4c210c00de3284c6b81cdba449dab04d85de585b Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 05:37:21 +0000 Subject: [PATCH 11/26] fix server tests --- .../nextjs/server-side-wrapper/server-side-wrapper.test.tsx | 4 +++- .../server/nextjs/server-side-wrapper/server-side-wrapper.tsx | 1 - 2 files changed, 3 insertions(+), 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 0576fa27..690c2121 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 @@ -13,9 +13,9 @@ describe("server-side-target", () => { darkTheme: "dark-blue", lightTheme: "light-yellow", colorSchemePref: "dark", + systemColorScheme: "dark" }), }, - "data-color-scheme-system": { value: "dark" }, }; globalThis.path = ""; }); @@ -61,6 +61,7 @@ describe("server-side-target", () => { ); expect(screen.getByTestId("server-side-target").getAttribute("data-theme")).toBe("light-yellow"); }); + test("forced color scheme system", ({ expect }) => { globalThis.path = "/forced-color-scheme/system"; render( @@ -70,6 +71,7 @@ describe("server-side-target", () => { ); expect(screen.getByTestId("server-side-target").getAttribute("data-theme")).toBe("dark-blue"); }); + test("force disable color scheme", ({ expect }) => { globalThis.path = "/forced-color-scheme"; render( 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 984f124b..1b7dbeb0 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 @@ -31,7 +31,6 @@ function sharedServerComponentRenderer( ? { forcedTheme: forcedPage[1].theme, forcedColorScheme: forcedPage[1].colorScheme } : forcedPage?.props; const themeState = state ? (JSON.parse(state) as ThemeStoreType) : undefined; - const isSystemDark = cookies().get("data-color-scheme-system")?.value === "dark"; const resolvedData = resolveTheme(themeState, forcedPageProps); const dataProps = getDataProps(resolvedData); From 56388d5b5e4c1ceea34e2dee6f905797b70da2cf Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 06:24:56 +0000 Subject: [PATCH 12/26] set up persistance and syncing --- .../client/theme-switcher/theme-switcher.tsx | 38 +++++++++++++++---- lib/nextjs-themes/src/utils.ts | 14 ++++++- 2 files changed, 43 insertions(+), 9 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 4a73a928..bbebf1cb 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 * as React from "react"; import { useEffect } from "react"; -import { resolveTheme } from "../../utils"; +import { resolveTheme, parseState, encodeState } from "../../utils"; import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../../constants"; import useRGS, { SetStateAction } from "r18gs"; @@ -27,6 +27,27 @@ function useMediaQuery(setThemeState: SetStateAction) { }, [setThemeState]); } +interface LoadSyncedStateProps { + targetSelector?: string; + setThemeState: SetStateAction; +} + +let tInit = 0; +function useLoadSyncedState({ targetSelector, setThemeState }: LoadSyncedStateProps) { + React.useEffect(() => { + tInit = Date.now(); + const key = targetSelector ?? DEFAULT_ID; + setThemeState(state => ({ ...state, ...parseState(localStorage.getItem(key)) })); + const storageListener = (e: StorageEvent) => { + if (e.key === key) setThemeState(state => ({ ...state, ...parseState(e.newValue) })); + }; + window.addEventListener("storage", storageListener); + return () => { + window.removeEventListener("storage", storageListener); + }; + }, [setThemeState, targetSelector]); +} + export interface DataProps { className: string; "data-th"?: string; @@ -44,7 +65,6 @@ export interface UpdateProps { const updateDOM = ( { resolvedTheme, resolvedColorScheme, resolvedColorSchemePref, th }: UpdateProps, - isSystemDark: boolean, targetSelector?: string, ) => { [document.querySelector(targetSelector || "#nextjs-themes"), document.documentElement].forEach(target => { @@ -56,9 +76,6 @@ const updateDOM = ( target?.setAttribute("data-color-scheme", resolvedColorScheme); target?.setAttribute("data-csp", resolvedColorSchemePref); /** color-scheme-preference */ }); - - /** store system preference for computing data-theme on server side */ - document.cookie = `data-color-scheme-system=${isSystemDark ? "dark" : "light"}`; }; const disableAnimation = (themeTransition = "none") => { @@ -88,13 +105,18 @@ export function useThemeSwitcher(props: ThemeSwitcherProps) { const [themeState, setThemeState] = useRGS(props.targetSelector ?? DEFAULT_ID, initialState); useMediaQuery(setThemeState); + useLoadSyncedState({targetSelector: props.targetSelector, setThemeState}) useEffect(() => { const restoreTransitions = disableAnimation(props.themeTransition); const resolvedData = resolveTheme(themeState, props); - updateDOM(resolvedData, themeState.systemColorScheme === "dark", props.targetSelector); - - restoreTransitions(); + updateDOM(resolvedData, props.targetSelector); + if (tInit < Date.now() - 300){ + const stateStr = encodeState(themeState); + const key = props.targetSelector || DEFAULT_ID; + localStorage.setItem(key, stateStr); + document.cookie = `${key}=${stateStr}; max-age=31536000; SameSite=Strict;`; + } restoreTransitions(); }, [props, themeState]); } diff --git a/lib/nextjs-themes/src/utils.ts b/lib/nextjs-themes/src/utils.ts index 6369fa5f..0078b198 100644 --- a/lib/nextjs-themes/src/utils.ts +++ b/lib/nextjs-themes/src/utils.ts @@ -1,5 +1,5 @@ import { ThemeSwitcherProps, UpdateProps } from "./client"; -import { ThemeStoreType } from "./constants"; +import { ColorSchemeType, ThemeStoreType, initialState } from "./constants"; export function resolveTheme(state?: ThemeStoreType, props?: ThemeSwitcherProps): UpdateProps { const resolvedForcedTheme = props?.forcedTheme === undefined ? state?.forcedTheme : props.forcedTheme; @@ -37,3 +37,15 @@ export function getResolvedTheme() { export function getResolvedColorScheme() { return document.documentElement.getAttribute("data-color-scheme"); } + +export function encodeState(themeState: ThemeStoreType) { + const { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme } = themeState; + return [colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme].join(","); +} + +export function 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; + return { colorSchemePref, systemColorScheme, darkTheme, lightTheme, theme }; +} From 47ab8fe42f0611aa76caa51eae20fbfbd2d9edf6 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 08:41:37 +0000 Subject: [PATCH 13/26] fix tests -- not loading synced state --- .../src/client/theme-switcher/theme-switcher.test.tsx | 2 -- .../src/client/theme-switcher/theme-switcher.tsx | 11 +++++++---- .../server-side-wrapper/server-side-wrapper.test.tsx | 6 ++++-- .../server-side-wrapper/server-side-wrapper.tsx | 10 +++++----- 4 files changed, 16 insertions(+), 13 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 72ebb0ce..b8fa5a4b 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 @@ -3,8 +3,6 @@ import { afterEach, describe, test } from "vitest"; import { useTheme } from "../../hooks"; import { ThemeSwitcher } from "./theme-switcher"; import { getResolvedColorScheme, getResolvedTheme } from "../../utils"; -import useRGS from "r18gs"; -import { DEFAULT_ID, initialState } from "../../constants"; /** * -> concurrency is not feasible because of global store conflicts 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 bbebf1cb..5bf1cd3e 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -45,7 +45,7 @@ function useLoadSyncedState({ targetSelector, setThemeState }: LoadSyncedStatePr return () => { window.removeEventListener("storage", storageListener); }; - }, [setThemeState, targetSelector]); + }, [targetSelector]); } export interface DataProps { @@ -104,19 +104,22 @@ const disableAnimation = (themeTransition = "none") => { export function useThemeSwitcher(props: ThemeSwitcherProps) { const [themeState, setThemeState] = useRGS(props.targetSelector ?? DEFAULT_ID, initialState); + console.log({ themeState }); + useMediaQuery(setThemeState); - useLoadSyncedState({targetSelector: props.targetSelector, setThemeState}) + // useLoadSyncedState({ targetSelector: props.targetSelector, setThemeState }); useEffect(() => { const restoreTransitions = disableAnimation(props.themeTransition); const resolvedData = resolveTheme(themeState, props); updateDOM(resolvedData, props.targetSelector); - if (tInit < Date.now() - 300){ + if (tInit < Date.now() - 300) { const stateStr = encodeState(themeState); const key = props.targetSelector || DEFAULT_ID; localStorage.setItem(key, stateStr); document.cookie = `${key}=${stateStr}; max-age=31536000; SameSite=Strict;`; - } restoreTransitions(); + } + restoreTransitions(); }, [props, themeState]); } 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 690c2121..af518be3 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 @@ -1,14 +1,16 @@ 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); beforeEach(() => { globalThis.cookies = { - "nextjs-themes": { - value: JSON.stringify({ + [DEFAULT_ID]: { + value: encodeState({ theme: "yellow", darkTheme: "dark-blue", lightTheme: "light-yellow", 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 1b7dbeb0..7e6beb1a 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 @@ -1,8 +1,8 @@ import * as React from "react"; import type { HTMLProps, ReactNode } from "react"; import { cookies, headers } from "next/headers"; -import type { ColorSchemeType, ThemeStoreType } from "../../../constants"; -import { resolveTheme } from "../../../utils"; +import { DEFAULT_ID, type ColorSchemeType, type ThemeStoreType } from "../../../constants"; +import { parseState, resolveTheme } from "../../../utils"; import { DataProps, UpdateProps } from "../../../client"; export type ForcedPage = @@ -21,7 +21,7 @@ function sharedServerComponentRenderer( defaultTag: "div" | "html", ) { const Tag: keyof JSX.IntrinsicElements = tag || defaultTag; - const state = cookies().get("nextjs-themes")?.value; + const state = cookies().get(DEFAULT_ID)?.value; const path = headers().get("referer"); const forcedPage = forcedPages?.find(forcedPage => @@ -30,13 +30,13 @@ function sharedServerComponentRenderer( const forcedPageProps = Array.isArray(forcedPage) ? { forcedTheme: forcedPage[1].theme, forcedColorScheme: forcedPage[1].colorScheme } : forcedPage?.props; - const themeState = state ? (JSON.parse(state) as ThemeStoreType) : undefined; + const themeState = state ? (parseState(state) as ThemeStoreType) : undefined; const resolvedData = resolveTheme(themeState, forcedPageProps); const dataProps = getDataProps(resolvedData); return ( // @ts-expect-error -> svg props and html element props conflict - + {children} ); From 550527b818724137c856fd48ead208ed56ec719e Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 10:43:07 +0000 Subject: [PATCH 14/26] fix tests --- .../theme-switcher/theme-switcher.test.tsx | 84 ++++++++++++++----- .../client/theme-switcher/theme-switcher.tsx | 11 +-- lib/nextjs-themes/src/hooks/use-theme.ts | 38 +++++---- lib/nextjs-themes/vitest.setup.ts | 6 -- 4 files changed, 91 insertions(+), 48 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 b8fa5a4b..88f519eb 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 @@ -1,8 +1,10 @@ -import { act, cleanup, render, renderHook } from "@testing-library/react"; -import { afterEach, describe, test } from "vitest"; +import { RenderHookResult, act, cleanup, fireEvent, render, renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, test } from "vitest"; import { useTheme } from "../../hooks"; import { ThemeSwitcher } from "./theme-switcher"; -import { getResolvedColorScheme, getResolvedTheme } from "../../utils"; +import { encodeState, getResolvedColorScheme, getResolvedTheme } from "../../utils"; +import useRGS, { SetterArgType } from "r18gs"; +import { DEFAULT_ID, ThemeStoreType, initialState } from "../../constants"; /** * -> concurrency is not feasible because of global store conflicts @@ -12,17 +14,35 @@ describe("theme-switcher", () => { cleanup(); }); - test("Test defaultDark and defaultLight themes", async ({ expect }) => { + let rgsHook: RenderHookResult<[ThemeStoreType, (val: SetterArgType) => void], unknown>; + + beforeEach(() => { + render(); + rgsHook = renderHook(() => useRGS(DEFAULT_ID)); + act(() => rgsHook.result.current[1](initialState)); + }); + + test("Test defaultLight theme", async ({ expect }) => { + const lightTheme = "light1"; + /** simulate changing lightTheme by another component in useEffect */ const { result } = renderHook(() => useTheme()); - act(() => result.current.setDarkTheme("dark1")); - act(() => result.current.setLightTheme("light1")); - window.media = "dark"; - await act(() => render()); - expect(getResolvedTheme()).toBe("dark1"); - window.media = "light"; - await act(() => render()); - expect(getResolvedTheme()).toBe("light1"); - expect(getResolvedColorScheme()).toBe("light"); + act(() => result.current.setLightTheme(lightTheme)); + /** await for the first time delay for setting state */ + await new Promise(res => setTimeout(res, 350)); + expect(getResolvedTheme()).toBe(lightTheme); + }); + + test("Test defaultDark theme", async ({ expect }) => { + const darkTheme = "dark1"; + /** simulate system dark mode */ + 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()); + act(() => result.current.setDarkTheme(darkTheme)); + /** await for the first time delay for setting state */ + await new Promise(res => setTimeout(res, 250)); + expect(getResolvedTheme()).toBe(darkTheme); }); // colorScheme has higher preference @@ -30,7 +50,7 @@ describe("theme-switcher", () => { const { result } = renderHook(() => useTheme()); act(() => result.current.setColorSchemePref("")); act(() => result.current.setTheme("blue")); - await act(() => render()); + await new Promise(res => setTimeout(res, 250)); expect(getResolvedTheme()).toBe("blue"); }); @@ -38,12 +58,12 @@ describe("theme-switcher", () => { const { result } = renderHook(() => useTheme()); act(() => result.current.setColorSchemePref("light")); act(() => result.current.setLightTheme("yellow")); + act(() => result.current.setDarkTheme("dark-blue")); act(() => result.current.setTheme("blue")); - await act(() => render()); + await new Promise(res => setTimeout(res, 250)); expect(getResolvedTheme()).toBe("yellow"); - act(() => result.current.setDarkTheme("dark-blue")); act(() => result.current.setColorSchemePref("dark")); - await act(() => render()); + /** note we do not require to await second time -- ?? what is user is setting theme from multuple useEffects? */ expect(getResolvedTheme()).toBe("dark-blue"); }); @@ -53,7 +73,7 @@ describe("theme-switcher", () => { act(() => result.current.setForcedColorScheme("dark")); act(() => result.current.setColorSchemePref("light")); act(() => result.current.setTheme("f1")); - await act(() => render()); + await new Promise(res => setTimeout(res, 250)); expect(getResolvedTheme()).toBe("forced1"); }); @@ -65,10 +85,36 @@ describe("theme-switcher", () => { act(() => result.current.setTheme("f1")); act(() => result.current.setLightTheme("yellow")); act(() => result.current.setDarkTheme("black")); - await act(() => render()); + await new Promise(res => setTimeout(res, 250)); expect(getResolvedTheme()).toBe(""); act(() => result.current.setForcedTheme(undefined)); expect(getResolvedTheme()).toBe("black"); + expect(getResolvedColorScheme()).toBe("dark"); + }); + + test("Storage event", async ({ expect }) => { + const hook = renderHook(() => useTheme()); + const MY_THEME = "my-theme-update"; + await act(() => + fireEvent( + window, + new StorageEvent("storage", { key: DEFAULT_ID, newValue: encodeState({ ...initialState, theme: MY_THEME }) }), + ), + ); + expect(hook.result.current.theme).toBe(MY_THEME); + }); +}); + +describe("theme-switcher with props", () => { + afterEach(() => { + cleanup(); + }); + + let rgsHook: RenderHookResult<[ThemeStoreType, (val: SetterArgType) => void], unknown>; + + beforeEach(() => { + rgsHook = renderHook(() => useRGS(DEFAULT_ID)); + act(() => rgsHook.result.current[1](initialState)); }); test("forced theme prop", async ({ expect }) => { 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 5bf1cd3e..d21fd04a 100644 --- a/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx +++ b/lib/nextjs-themes/src/client/theme-switcher/theme-switcher.tsx @@ -27,13 +27,8 @@ function useMediaQuery(setThemeState: SetStateAction) { }, [setThemeState]); } -interface LoadSyncedStateProps { - targetSelector?: string; - setThemeState: SetStateAction; -} - let tInit = 0; -function useLoadSyncedState({ targetSelector, setThemeState }: LoadSyncedStateProps) { +function useLoadSyncedState(setThemeState: SetStateAction, targetSelector?: string) { React.useEffect(() => { tInit = Date.now(); const key = targetSelector ?? DEFAULT_ID; @@ -104,10 +99,8 @@ const disableAnimation = (themeTransition = "none") => { export function useThemeSwitcher(props: ThemeSwitcherProps) { const [themeState, setThemeState] = useRGS(props.targetSelector ?? DEFAULT_ID, initialState); - console.log({ themeState }); - useMediaQuery(setThemeState); - // useLoadSyncedState({ targetSelector: props.targetSelector, setThemeState }); + useLoadSyncedState(setThemeState, props.targetSelector); useEffect(() => { const restoreTransitions = disableAnimation(props.themeTransition); diff --git a/lib/nextjs-themes/src/hooks/use-theme.ts b/lib/nextjs-themes/src/hooks/use-theme.ts index b45c6eb2..7ef90467 100644 --- a/lib/nextjs-themes/src/hooks/use-theme.ts +++ b/lib/nextjs-themes/src/hooks/use-theme.ts @@ -1,30 +1,40 @@ -import useRGS from "r18gs"; +import useRGS, { SetStateAction } from "r18gs"; import { ColorSchemeType, DEFAULT_ID, ThemeStoreType, initialState } from "../constants"; import { resolveTheme } from "../utils"; -import { useCallback } from "react"; +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) { + 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; + }; +} export function useTheme(targetId?: string) { const [themeState, setThemeState] = useRGS(targetId ?? DEFAULT_ID, initialState); const { resolvedColorScheme, resolvedTheme } = resolveTheme(themeState); + const setterWithFirstTimeDelay = useMemo(() => createSetterWithFirstTimeDelay(setThemeState), []); return { ...themeState, resolvedColorScheme, resolvedTheme, - setTheme: useCallback((theme: string) => setThemeState(state => ({ ...state, theme })), []), - setDarkTheme: useCallback((darkTheme: string) => setThemeState(state => ({ ...state, darkTheme })), []), - setLightTheme: useCallback((lightTheme: string) => setThemeState(state => ({ ...state, lightTheme })), []), + setTheme: setterWithFirstTimeDelay("theme"), + setDarkTheme: setterWithFirstTimeDelay("darkTheme"), + setLightTheme: setterWithFirstTimeDelay("lightTheme"), setThemeSet: useCallback( (themeSet: { darkTheme: string; lightTheme: string }) => setThemeState(state => ({ ...state, ...themeSet })), [], ), - setColorSchemePref: useCallback( - (colorSchemePref: ColorSchemeType) => setThemeState(state => ({ ...state, colorSchemePref })), - [], - ), - setForcedTheme: useCallback((forcedTheme?: string) => setThemeState(state => ({ ...state, forcedTheme })), []), - setForcedColorScheme: useCallback( - (forcedColorScheme?: ColorSchemeType) => setThemeState(state => ({ ...state, forcedColorScheme })), - [], - ), + setColorSchemePref: setterWithFirstTimeDelay("colorSchemePref"), + setForcedTheme: setterWithFirstTimeDelay("forcedTheme"), + setForcedColorScheme: setterWithFirstTimeDelay("forcedColorScheme"), }; } diff --git a/lib/nextjs-themes/vitest.setup.ts b/lib/nextjs-themes/vitest.setup.ts index 2096517b..6480d5d5 100644 --- a/lib/nextjs-themes/vitest.setup.ts +++ b/lib/nextjs-themes/vitest.setup.ts @@ -35,9 +35,3 @@ vi.mock("next/headers", () => ({ cookies: () => ({ get: (cookieName: string) => globalThis.cookies[cookieName] }), headers: () => ({ get: (h: string) => globalThis.path }), })); - -/** reset global state */ -beforeEach(() => { - const { result } = renderHook(() => useRGS(DEFAULT_ID)); - act(() => result.current[1](initialState)); -}); From de9b62fd0d2b73e4c4b28763f4ede8a5e14f7817 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 25 Feb 2024 10:57:55 +0000 Subject: [PATCH 15/26] update shared ui --- README.md | 3 --- .../simple-multi-theme/app/ThemeSelector.tsx | 2 +- lib/nextjs-themes/src/index.ts | 1 + .../color-scheme-preference.tsx | 2 +- .../src/theme-controller/theme-selector.tsx | 20 +++++++++---------- turbo.json | 1 - 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8d6e192d..bfede642 100644 --- a/README.md +++ b/README.md @@ -221,9 +221,6 @@ In case your components need to know the current theme and be able to change it. import { useTheme } from "nextjs-themes"; const ThemeChanger = () => { - /* you can also improve performance by using selectors - * const [theme, setTheme] = useTheme(state => [state.theme, state.setTheme]); - */ const { theme, setTheme } = useTheme(); return ( diff --git a/examples/simple-multi-theme/app/ThemeSelector.tsx b/examples/simple-multi-theme/app/ThemeSelector.tsx index 7a9e08e3..bd7ef91e 100644 --- a/examples/simple-multi-theme/app/ThemeSelector.tsx +++ b/examples/simple-multi-theme/app/ThemeSelector.tsx @@ -4,7 +4,7 @@ import { darkThemes, lightThemes } from "shared-ui"; import styles from "shared-ui/src/root-layout.module.css"; export default function ThemeSelector() { - const [theme, setTheme] = useTheme(state => [state.theme, state.setTheme]); + const { theme, setTheme } = useTheme(); return (