From 22b42cf545bcb3c80e4531be957a97bd44167694 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Sun, 11 Feb 2024 16:01:50 +0800 Subject: [PATCH] feat: support dark mode, close #710 --- package.json | 1 + pnpm-lock.yaml | 32 ++++++- src/lib/components/AppearanceSwitch.tsx | 13 +++ src/lib/components/Button.tsx | 2 +- src/lib/hooks/use-dark.ts | 67 ++++++++++++++ src/lib/style.css | 112 ++++++++++++------------ src/options/Siderbar.tsx | 10 ++- src/options/index.tsx | 13 +-- src/options/routes/About.tsx | 9 +- src/options/routes/General.tsx | 2 +- src/options/routes/Rules.tsx | 4 +- src/popup/RSSItem.tsx | 2 +- src/popup/index.tsx | 9 +- 13 files changed, 193 insertions(+), 83 deletions(-) create mode 100644 src/lib/components/AppearanceSwitch.tsx create mode 100644 src/lib/hooks/use-dark.ts diff --git a/package.json b/package.json index a16f9b7..9aa1cd2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "async-lock": "^1.4.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "foxact": "^0.2.31", "he": "1.2.0", "lodash": "^4.17.21", "lucide-react": "^0.320.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e76808..296ab0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@iconify-json/mingcute': specifier: ^1.1.15 @@ -35,6 +31,9 @@ dependencies: clsx: specifier: ^2.1.0 version: 2.1.0 + foxact: + specifier: ^0.2.31 + version: 0.2.31(react@18.2.0) he: specifier: 1.2.0 version: 1.2.0 @@ -3863,6 +3862,10 @@ packages: engines: {node: '>= 12'} dev: false + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -4496,6 +4499,19 @@ packages: fetch-blob: 3.2.0 dev: true + /foxact@0.2.31(react@18.2.0): + resolution: {integrity: sha512-NxO/aw7e7wFf+xp8JQi8lDrfrKtIDsWwiFHRBajCO+2oLLgupxSqZV++/amOzxC9dmanDHBN3O2YHGv2LJsJrA==} + peerDependencies: + react: '*' + peerDependenciesMeta: + react: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.2.0 + server-only: 0.0.1 + dev: false + /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true @@ -6602,6 +6618,10 @@ packages: dependencies: lru-cache: 6.0.0 + /server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + dev: false + /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} engines: {node: '>= 0.4'} @@ -7369,3 +7389,7 @@ packages: /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/src/lib/components/AppearanceSwitch.tsx b/src/lib/components/AppearanceSwitch.tsx new file mode 100644 index 0000000..1f6aac5 --- /dev/null +++ b/src/lib/components/AppearanceSwitch.tsx @@ -0,0 +1,13 @@ +import { useDark } from "~/lib/hooks/use-dark" + +export function AppearanceSwitch({ className = "" }: { className?: string }) { + const { toggleDark } = useDark() + + return ( + + ) +} diff --git a/src/lib/components/Button.tsx b/src/lib/components/Button.tsx index 694d26b..2055406 100644 --- a/src/lib/components/Button.tsx +++ b/src/lib/components/Button.tsx @@ -19,7 +19,7 @@ const buttonVariants = cva( "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", - rss: "border border-orange-500 text-orange-500 bg-background hover:bg-orange-500 hover:text-white", + rss: "border border-primary text-primary bg-background hover:bg-primary hover:text-white", }, size: { default: "h-10 px-4 py-2", diff --git a/src/lib/hooks/use-dark.ts b/src/lib/hooks/use-dark.ts new file mode 100644 index 0000000..daa701b --- /dev/null +++ b/src/lib/hooks/use-dark.ts @@ -0,0 +1,67 @@ +import { useLocalStorage } from "foxact/use-local-storage" +import { useEffect, useMemo, useSyncExternalStore } from "react" + +const query = "(prefers-color-scheme: dark)" + +function getSnapshot() { + return window.matchMedia(query).matches +} + +function getServerSnapshot(): undefined { + return undefined +} + +function subscribe(callback: () => void) { + const matcher = window.matchMedia(query) + matcher.addEventListener("change", callback) + return () => { + matcher.removeEventListener("change", callback) + } +} + +function useSystemDark() { + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) +} + +const themeOptions = ["system", "light", "dark"] as const +export type Theme = (typeof themeOptions)[number] + +function isDarkMode(setting?: Theme | null, isSystemDark?: boolean) { + return setting === "dark" || (isSystemDark && setting !== "light") +} + +export function useDark(themeKey = "use-dark") { + const [theme, setTheme] = useLocalStorage(themeKey, "system") + const isSystemDark = useSystemDark() + + const isDark = useMemo( + () => isDarkMode(theme, isSystemDark), + [isSystemDark, theme], + ) + + const toggleDark = () => { + if (theme === "system") { + setTheme(isSystemDark ? "light" : "dark") + } else { + setTheme("system") + } + } + + useEffect(() => { + const isDark = isDarkMode(theme, isSystemDark) + if (isDark) { + document.documentElement.classList.toggle("dark", true) + } else { + document.documentElement.classList.toggle("dark", false) + } + + if ( + (theme === "dark" && isSystemDark) || + (theme === "light" && !isSystemDark) + ) { + setTheme("system") + } + }, [theme, isSystemDark, setTheme]) + + return { isDark, toggleDark } +} diff --git a/src/lib/style.css b/src/lib/style.css index 05b052a..255fd02 100644 --- a/src/lib/style.css +++ b/src/lib/style.css @@ -2,62 +2,62 @@ @tailwind components; @tailwind utilities; -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 20 14.3% 4.1%; - --card: 0 0% 100%; - --card-foreground: 20 14.3% 4.1%; - --popover: 0 0% 100%; - --popover-foreground: 20 14.3% 4.1%; - --primary: 24.6 95% 53.1%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 60 4.8% 95.9%; - --secondary-foreground: 24 9.8% 10%; - --muted: 60 4.8% 95.9%; - --muted-foreground: 25 5.3% 44.7%; - --accent: 60 4.8% 95.9%; - --accent-foreground: 24 9.8% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 20 5.9% 90%; - --input: 20 5.9% 90%; - --ring: 24.6 95% 53.1%; - --radius: 0.5rem; - } - - .dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 20.5 90.2% 48.2%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 12 6.5% 15.1%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 72.2% 50.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; - --ring: 20.5 90.2% 48.2%; - } +:root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 24.6 95% 53.1%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 24.6 95% 53.1%; + --radius: 0.5rem; } -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } - - .content a { - @apply underline text-orange-500; - } +.dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 20.5 90.2% 48.2%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 20.5 90.2% 48.2%; +} + +.dark { + color-scheme: dark; +} + +* { + @apply border-border; +} +body { + @apply bg-background text-foreground; +} + +.content a { + @apply underline text-primary; } diff --git a/src/options/Siderbar.tsx b/src/options/Siderbar.tsx index d72bfdc..58d45ed 100644 --- a/src/options/Siderbar.tsx +++ b/src/options/Siderbar.tsx @@ -1,6 +1,7 @@ import RSSHubIcon from "data-base64:~/assets/icon.png" import { Link, useLocation } from "react-router-dom" +import { AppearanceSwitch } from "~/lib/components/AppearanceSwitch" import { cn } from "~/lib/utils" import info from "../../package.json" @@ -28,7 +29,7 @@ function Siderbar() { return (
-
+
RSSHub Radar
@@ -40,9 +41,9 @@ function Siderbar() { to={link.path} className={cn( location.pathname === link.path - ? "bg-orange-50 text-orange-500" + ? "bg-primary/10 text-primary" : "", - "px-4 py-3 hover:bg-orange-50 transition-colors rounded-lg flex items-center space-x-2", + "px-4 py-3 hover:bg-primary/10 transition-colors rounded-lg flex items-center space-x-2", )} > @@ -52,10 +53,11 @@ function Siderbar() { ))}