diff --git a/package.json b/package.json
index 03c83386..894f4f1e 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
"mermaid": "10.3.1",
"nanoid": "4.0.2",
"next": "13.4.12",
+ "next-themes": "0.2.1",
"node-id3": "0.2.6",
"open-graph-scraper": "^6.2.2",
"pangu": "4.0.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 991ad330..8328dbed 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -206,6 +206,9 @@ dependencies:
next:
specifier: 13.4.12
version: 13.4.12(@babel/core@7.22.10)(react-dom@18.2.0)(react@18.2.0)
+ next-themes:
+ specifier: 0.2.1
+ version: 0.2.1(next@13.4.12)(react-dom@18.2.0)(react@18.2.0)
node-id3:
specifier: 0.2.6
version: 0.2.6
@@ -9214,6 +9217,18 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
+ /next-themes@0.2.1(next@13.4.12)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
+ peerDependencies:
+ next: '*'
+ react: '*'
+ react-dom: '*'
+ dependencies:
+ next: 13.4.12(@babel/core@7.22.10)(react-dom@18.2.0)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/next@13.4.12(@babel/core@7.22.10)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==}
engines: {node: '>=16.8.0'}
diff --git a/src/app/ColorSchemeInjector.tsx b/src/app/ColorSchemeInjector.tsx
index b6850548..d56fb426 100644
--- a/src/app/ColorSchemeInjector.tsx
+++ b/src/app/ColorSchemeInjector.tsx
@@ -1,33 +1,16 @@
"use client"
-import { useServerInsertedHTML } from "next/navigation"
-
-import {
- COLOR_SCHEME_DARK,
- COLOR_SCHEME_LIGHT,
- DARK_MODE_STORAGE_KEY,
-} from "~/lib/constants"
+import { DARK_MODE_STORAGE_KEY } from "~/lib/constants"
export const ColorSchemeInjector = () => {
- useServerInsertedHTML(() => {
- return (
-
- )
- })
- return null
+ }}
+ >
+ )
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index de559f82..8c2f4e85 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -115,7 +115,9 @@ export default function RootLayout({
className={colorScheme}
suppressHydrationWarning
>
-
+
+
+
{modal}
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
index 72ae562b..bd4381be 100644
--- a/src/app/providers.tsx
+++ b/src/app/providers.tsx
@@ -1,5 +1,6 @@
"use client"
+import { ThemeProvider } from "next-themes"
import { useState } from "react"
import { WagmiConfig } from "wagmi"
@@ -11,10 +12,11 @@ import {
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ModalStackProvider } from "~/components/ui/ModalStack"
+import { useDarkModeListener } from "~/hooks/useDarkMode"
// eslint-disable-next-line import/no-unresolved
-import { useDarkMode } from "~/hooks/useDarkMode"
import { useMobileLayout } from "~/hooks/useMobileLayout"
import { useNProgress } from "~/hooks/useNProgress"
+import { DARK_MODE_STORAGE_KEY } from "~/lib/constants"
import { APP_NAME, WALLET_CONNECT_V2_PROJECT_ID } from "~/lib/env"
import { filterNotificationCharacter } from "~/lib/filter-character"
import { toGateway } from "~/lib/ipfs-parser"
@@ -42,31 +44,37 @@ export default function Providers({
children: React.ReactNode
lang: string
}) {
- useDarkMode()
useMobileLayout()
useNProgress()
+ useDarkModeListener()
const [queryClient] = useState(createQueryClient)
return (
-
-
-
-
- {children}
-
+
+
+
+
+
+ {children}
+
-
-
-
-
+
+
+
+
+
)
}
diff --git a/src/components/common/DarkModeSwitch.tsx b/src/components/common/DarkModeSwitch.tsx
index 5ec40c1e..00e31e7f 100644
--- a/src/components/common/DarkModeSwitch.tsx
+++ b/src/components/common/DarkModeSwitch.tsx
@@ -1,34 +1,130 @@
"use client"
-import { Switch } from "@headlessui/react"
+import { useTheme } from "next-themes"
+import { MouseEvent } from "react"
-import { useDarkModeSwitch, useIsDark } from "~/hooks/useDarkMode"
-import { cn } from "~/lib/utils"
+import { useIsClient } from "~/hooks/useClient"
-export const DarkModeSwitch = () => {
- const toggle = useDarkModeSwitch()
- const isDark = useIsDark()
+const styles = {
+ base: " rounded-inherit inline-flex h-[32px] w-[32px] items-center justify-center border-0 text-current",
+}
+
+const ThemeIndicator = () => {
+ const { theme } = useTheme()
+
+ const isClient = useIsClient()
+
+ if (!isClient) return null
+ if (!theme) return null
return (
- toggle(e)}
- className={cn(
- `${isDark ? "bg-accent" : "bg-gray-200"}`,
- ` relative inline-flex h-6 w-11 items-center rounded-full text-base dark:text-always-gray-200 text-always-gray-700 align-middle`,
- )}
- >
- Switch Dark Mode
-
-
- {isDark ? (
-
- ) : (
-
- )}
-
+
+ )
+}
+
+const ThemeSwitcher = () => {
+ const { setTheme } = useTheme()
+
+ const buildThemeTransition = (
+ e: MouseEvent,
+ theme: "light" | "dark" | "system",
+ ) => {
+ return setTheme(theme)
+ // disable in https://github.com/Crossbell-Box/xLog/commit/06713c901e90b57de6f5f5a0252d2c9dd874335f
+ if (
+ !("startViewTransition" in document) ||
+ window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
+ ) {
+ setTheme(theme)
+ return
+ }
+
+ const $document = document.documentElement
+
+ const { x, y } = e.currentTarget.getBoundingClientRect()
+
+ const endRadius = Math.hypot(
+ Math.max(x, window.innerWidth - x),
+ Math.max(y, window.innerHeight - y),
+ )
+
+ document
+ .startViewTransition(() => {
+ return Promise.resolve()
+ })
+ ?.ready.then(() => {
+ setTheme(theme)
+
+ if (x === 0) return
+ const clipPath = [
+ `circle(0px at ${x}px ${y}px)`,
+ `circle(${endRadius}px at ${x}px ${y}px)`,
+ ]
+
+ $document.animate(
+ {
+ clipPath,
+ },
+ {
+ duration: 300,
+ easing: "ease-in",
+ pseudoElement: "::view-transition-new(root)",
+ // pseudoElement: darkMode
+ // ? "::view-transition-old(root)"
+ // : "::view-transition-new(root)",
+ },
+ )
+ })
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+export const DarkModeSwitch = () => {
+ return (
+
+
+
+
)
}
diff --git a/src/hooks/useDarkMode.ts b/src/hooks/useDarkMode.ts
index 2214cce8..28c428ce 100644
--- a/src/hooks/useDarkMode.ts
+++ b/src/hooks/useDarkMode.ts
@@ -1,231 +1,31 @@
-import { MouseEvent, useEffect, useRef, useState } from "react"
-import { create } from "zustand"
+import { useTheme } from "next-themes"
+import { useEffect } from "react"
-import {
- COLOR_SCHEME_DARK,
- COLOR_SCHEME_LIGHT,
- DARK_MODE_STORAGE_KEY,
- DEFAULT_COLOR_SCHEME,
- IS_DEV,
-} from "~/lib/constants"
+import { COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT, IS_DEV } from "~/lib/constants"
import { OUR_DOMAIN } from "~/lib/env"
-import { noop } from "~/lib/noop"
-import { delStorage, getStorage, setStorage } from "~/lib/storage"
-import { isServerSide } from "~/lib/utils"
-
-import { useGetState } from "./useGetState"
-
-interface IMediaStore {
- isDark: boolean
- toggle: (e: MouseEvent) => void
-}
-
-const useMediaStore = create(() => {
- return {
- isDark: DEFAULT_COLOR_SCHEME === COLOR_SCHEME_DARK,
- toggle: () => void 0,
- }
-})
-
-interface DarkModeConfig {
- classNameDark?: string // A className to set "dark mode". Default = "dark".
- classNameLight?: string // A className to set "light mode". Default = "light".
- element?: HTMLElement | undefined | null // The element to apply the className. Default = `document.body`.
- transition?: ViewTransition | undefined // Specify the `animate` when switching the mode. Only Chromium >= 111 etc.
-}
-
-const useDarkModeInternal = (
- initialState: boolean | undefined,
- options: DarkModeConfig,
-) => {
- const {
- classNameDark = COLOR_SCHEME_DARK,
- classNameLight = COLOR_SCHEME_LIGHT,
- element,
- transition,
- } = options
-
- const [darkMode, setDarkMode] = useState(initialState)
- const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
+export const useDarkModeListener = () => {
+ const { theme, systemTheme } = useTheme()
useEffect(() => {
- const presentedDarkMode =
- !isServerSide() && getStorage(DARK_MODE_STORAGE_KEY)
+ if (theme === "system") {
+ // delete cookie
+ document.cookie = IS_DEV
+ ? `color_scheme=;`
+ : `color_scheme=; Domain=.${OUR_DOMAIN}; Path=/; Secure; expires=Thu, 01 Jan 1970 00:00:00 GMT`
- if (presentedDarkMode !== undefined) {
- setDarkMode(presentedDarkMode === "true")
- } else if (typeof initialState === "undefined") {
- setDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches)
- }
- }, [])
-
- useEffect(() => {
- const handler = (e: MediaQueryListEvent) => {
- const storageValue = getStorage(DARK_MODE_STORAGE_KEY)
- const parseStorageValueAsBool = storageValue === "true"
- setDarkMode(e.matches)
-
- // reset dark mode, follow system
- if (parseStorageValueAsBool === e.matches) {
- delStorage(DARK_MODE_STORAGE_KEY)
- }
- }
-
- const storageHandler = () => {
- const storageValue = getStorage(DARK_MODE_STORAGE_KEY, true)
- // if not storage color mode, switch to follow system
- if (storageValue === undefined) {
- setDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches)
- } else {
- // make multiple pages to switch to dark mode together.
- setDarkMode(storageValue === "true")
- }
- }
-
- window.addEventListener("storage", storageHandler)
- window
- .matchMedia("(prefers-color-scheme: dark)")
- .addEventListener("change", handler)
-
- return () => {
- window.removeEventListener("storage", storageHandler)
- window
- .matchMedia("(prefers-color-scheme: dark)")
- .removeEventListener("change", handler)
- }
- }, [])
-
- const getDarkMode = useGetState(darkMode)
- useEffect(() => {
- const handler = () => {
- // if set color mode follow system, del storage
- if (
- window.matchMedia("(prefers-color-scheme: dark)").matches ===
- getDarkMode()
- ) {
- delStorage(DARK_MODE_STORAGE_KEY)
- }
- }
- window.addEventListener("beforeunload", handler)
-
- return () => {
- window.removeEventListener("beforeunload", handler)
- }
- }, [])
- useEffect(() => {
- if (isServerSide() || typeof darkMode === "undefined") {
return
}
- const $document = element || document.documentElement
- const setDarkModeClass = () => {
- if (darkMode) {
- $document.classList.remove(classNameLight)
- $document.classList.add(classNameDark)
- } else {
- $document.classList.remove(classNameDark)
- $document.classList.add(classNameLight)
- }
- }
-
- const { x, y } = mousePosition
- const endRadius = Math.hypot(
- Math.max(x, innerWidth - x),
- Math.max(y, innerHeight - y),
- )
- transition?.ready.then(() => {
- setDarkModeClass()
- if (mousePosition.x === 0) return
- const clipPath = [
- `circle(0px at ${x}px ${y}px)`,
- `circle(${endRadius}px at ${x}px ${y}px)`,
- ]
- $document.animate(
- {
- clipPath: !darkMode ? clipPath : [...clipPath].reverse(),
- },
- {
- duration: 400,
- easing: "ease-in-out",
- pseudoElement: darkMode
- ? "::view-transition-old(root)"
- : "::view-transition-new(root)",
- },
- )
- }) ?? setDarkModeClass()
- }, [classNameDark, classNameLight, darkMode, element])
-
- if (isServerSide()) {
- return {
- toggle: () => {},
- value: false,
- }
- }
-
- return {
- value: darkMode,
- toggle: (e: MouseEvent) => {
- setDarkMode((d) => {
- if (DARK_MODE_STORAGE_KEY && !isServerSide()) {
- setStorage(DARK_MODE_STORAGE_KEY, String(!d))
- setMousePosition({ x: e.clientX, y: e.clientY })
- }
- return !d
- })
- },
- }
-}
-
-const mockElement = {
- classList: {
- add: noop,
- remove: noop,
- },
-}
-
-export const useDarkMode = () => {
- const { toggle, value } = useDarkModeInternal(
- getStorage(DARK_MODE_STORAGE_KEY) === undefined
- ? undefined
- : getStorage(DARK_MODE_STORAGE_KEY) === "true",
- {
- classNameDark: COLOR_SCHEME_DARK,
- classNameLight: COLOR_SCHEME_LIGHT,
- element: (globalThis.document && document.documentElement) || mockElement,
- // transition:
- // !isServerSide() &&
- // !!document.startViewTransition &&
- // !window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
- // ? document.startViewTransition()
- // : undefined,
- },
- )
-
- useEffect(() => {
- useMediaStore.setState({
- isDark: value,
- })
- const colorScheme = value ? COLOR_SCHEME_DARK : COLOR_SCHEME_LIGHT
+ const isDarkMode = theme === "dark"
+ const colorScheme = isDarkMode ? COLOR_SCHEME_DARK : COLOR_SCHEME_LIGHT
const date = new Date()
date.setMonth(date.getMonth() + 1)
document.cookie = IS_DEV
? `color_scheme=${colorScheme};`
: `color_scheme=${colorScheme}; Domain=.${OUR_DOMAIN}; Path=/; Secure; expires=${date.toUTCString()}`
- }, [value])
-
- const onceRef = useRef(false)
- if (!onceRef.current) {
- onceRef.current = true
- useMediaStore.setState({ toggle })
- }
-
- return {
- toggle,
- value,
- }
+ }, [theme, systemTheme])
}
-export const useIsDark = () => useMediaStore((state) => state.isDark)
-
-export const useDarkModeSwitch = () => {
- return useMediaStore((state) => state.toggle)
+export const useIsDark = () => {
+ const { theme, systemTheme } = useTheme()
+ return theme === "dark" || (theme === "system" && systemTheme === "dark")
}