diff --git a/package.json b/package.json index 5f36b61..ce35eb2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@cloudflare/next-on-pages": "^1.13.3", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-select": "^2.1.1", @@ -22,6 +23,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.446.0", "next": "^14.2.4", + "next-themes": "^0.3.0", "polished": "^4.3.1", "random": "^5.1.0", "react": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9bfecf..d779ed2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@cloudflare/next-on-pages': specifier: ^1.13.3 version: 1.13.3(@cloudflare/workers-types@4.20240925.0)(vercel@37.6.0)(wrangler@3.78.12(@cloudflare/workers-types@4.20240925.0)) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) @@ -44,6 +47,9 @@ importers: next: specifier: ^14.2.4 version: 14.2.13(@playwright/test@1.47.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: + specifier: ^0.3.0 + version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) polished: specifier: ^4.3.1 version: 4.3.1 @@ -604,6 +610,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.1': + resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.0': resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} peerDependencies: @@ -640,6 +659,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-menu@2.1.1': + resolution: {integrity: sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-navigation-menu@1.2.0': resolution: {integrity: sha512-OQ8tcwAOR0DhPlSY3e4VMXeHiol7la4PPdJWhhwJiJA+NLX0SaCaonOkRnI3gCDHoZ7Fo7bb/G6q25fRM2Y+3Q==} peerDependencies: @@ -705,6 +737,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.0': + resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-select@2.1.1': resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==} peerDependencies: @@ -2465,6 +2510,12 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-themes@0.3.0: + resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} + peerDependencies: + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + next@14.2.13: resolution: {integrity: sha512-BseY9YNw8QJSwLYD7hlZzl6QVDoSFHL/URN5K64kVEVpCsSOWeyjbIGK+dZUaRViHTaMQX8aqmnn0PHBbGZezg==} engines: {node: '>=18.17.0'} @@ -3837,6 +3888,21 @@ snapshots: '@types/react': 18.3.10 '@types/react-dom': 18.3.0 + '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.10 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: react: 18.3.1 @@ -3865,6 +3931,32 @@ snapshots: optionalDependencies: '@types/react': 18.3.10 + '@radix-ui/react-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.10)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.10 + '@types/react-dom': 18.3.0 + '@radix-ui/react-navigation-menu@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -3934,6 +4026,23 @@ snapshots: '@types/react': 18.3.10 '@types/react-dom': 18.3.0 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.10 + '@types/react-dom': 18.3.0 + '@radix-ui/react-select@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.0 @@ -5913,6 +6022,11 @@ snapshots: natural-compare@1.4.0: {} + next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + next@14.2.13(@playwright/test@1.47.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.13 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3ba75ec..9c0694e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { GeistSans } from 'geist/font/sans' import { type Metadata } from 'next' import { Background } from '~/components/background' +import { ThemeProvider } from '~/components/theme-provider' export const metadata: Metadata = { title: 'Create T3 App', @@ -15,10 +16,21 @@ export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - + - - {children} + + + {children} + ) diff --git a/src/app/page.tsx b/src/app/page.tsx index 460d7d6..c6b968f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,9 @@ +import { ThemeChooser } from '~/components/theme-chooser' + export default function HomePage() { return ( -
+
+ +
) } diff --git a/src/components/background.tsx b/src/components/background.tsx index 46d7c66..75261e1 100644 --- a/src/components/background.tsx +++ b/src/components/background.tsx @@ -2,6 +2,7 @@ import debounce from 'lodash/debounce' import times from 'lodash/times' +import { useTheme } from 'next-themes' import { rgba } from 'polished' import random from 'random' import { useEffect, useRef } from 'react' @@ -57,10 +58,7 @@ const getRandomXY = (w: number, h: number) => { export const Background = () => { const canvas = useRef(null) - - // const theme = useContext(ThemeContext) - - const drawCanvasRef = useRef<() => void>() + const { theme } = useTheme() useEffect(() => { const drawCanvas = () => { @@ -68,7 +66,6 @@ export const Background = () => { document.documentElement, ).getPropertyValue('--foreground')})` - console.log(foreground) if (!canvas.current) { return } @@ -97,14 +94,15 @@ export const Background = () => { }) } - if (drawCanvasRef.current) { - window.removeEventListener('resize', drawCanvasRef.current) - } - drawCanvasRef.current = debounce(drawCanvas, 100) + const debouncedDrawCanvas = debounce(drawCanvas, 100) - drawCanvas() - window.addEventListener('resize', drawCanvasRef.current) - }, [canvas]) + // in current approach, the color is read from computed style, need to wait for theme change happen in DOM + requestAnimationFrame(drawCanvas) + window.addEventListener('resize', debouncedDrawCanvas) + return () => { + window.removeEventListener('resize', debouncedDrawCanvas) + } + }, [theme]) return } diff --git a/src/components/theme-chooser.tsx b/src/components/theme-chooser.tsx new file mode 100644 index 0000000..a97e268 --- /dev/null +++ b/src/components/theme-chooser.tsx @@ -0,0 +1,43 @@ +'use client' + +import { MoonIcon, SunIcon } from 'lucide-react' +import { useTheme } from 'next-themes' + +import { Button } from '~/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '~/components/ui/dropdown-menu' + +export const ThemeChooser = () => { + const { theme, setTheme } = useTheme() + + console.log('theme', theme) + + return ( + + + + + + + + + 跟随系统 + + + + ) +} diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 0000000..b83445d --- /dev/null +++ b/src/components/theme-provider.tsx @@ -0,0 +1,8 @@ +'use client' + +import { ThemeProvider as NextThemesProvider } from 'next-themes' +import { type ThemeProviderProps } from 'next-themes/dist/types' + +export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => { + return {children} +} diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..4887f81 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,205 @@ +'use client' + +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from '@radix-ui/react-icons' + +import { cn } from '~/lib/utils' + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}