diff --git a/package.json b/package.json index 063eefd..f88c679 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebf5c87..e1bd3ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-radio-group': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-scroll-area': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0) @@ -1682,6 +1685,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.5 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.39)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.39)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.39)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.39)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.39)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.39)(react@18.2.0) + '@types/react': 18.2.39 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: diff --git a/src/app/options/components/ProfileForm.tsx b/src/app/options/components/ProfileForm.tsx index 80a6bec..64e252c 100644 --- a/src/app/options/components/ProfileForm.tsx +++ b/src/app/options/components/ProfileForm.tsx @@ -1,4 +1,4 @@ -import { object, string, type Output, minBytes, maxBytes, toTrimmed, boolean, notLength } from 'valibot' +import { object, string, type Output, minBytes, maxBytes, toTrimmed, union, literal, notLength } from 'valibot' import { useForm } from 'react-hook-form' import { valibotResolver } from '@hookform/resolvers/valibot' @@ -10,8 +10,10 @@ import AvatarSelect from './AvatarSelect' import { Button } from '@/components/ui/Button' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form' import { Input } from '@/components/ui/Input' -import { Switch } from '@/components/ui/Switch' import UserInfoDomain from '@/domain/UserInfo' +import { checkSystemDarkMode } from '@/utils' +import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup' +import { Label } from '@/components/ui/Label' // In chrome storage.sync, each key-value pair supports a maximum storage of 8kb // Image is encoded as base64, and the size is increased by about 33%. @@ -25,9 +27,16 @@ const formSchema = object({ maxBytes(20, 'Your username cannot exceed 20 bytes.') ]), avatar: string([notLength(0, 'Please select your avatar.'), maxBytes(8 * 1024, 'Your avatar cannot exceed 8kb.')]), - darkMode: boolean() + themeMode: union([literal('system'), literal('light'), literal('dark')]) }) +const defaultUserInfo: UserInfo = { + id: nanoid(), + name: '', + avatar: '', + themeMode: checkSystemDarkMode() ? 'dark' : 'system' +} + const ProfileForm = () => { const send = useRemeshSend() const userInfoDomain = useRemeshDomain(UserInfoDomain()) @@ -35,12 +44,7 @@ const ProfileForm = () => { const form = useForm({ resolver: valibotResolver(formSchema), - defaultValues: userInfo ?? { - id: nanoid(), - name: '', - avatar: '', - darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches - } + defaultValues: userInfo ?? defaultUserInfo }) useEffect(() => { @@ -97,17 +101,30 @@ const ProfileForm = () => { /> ( - DarkMode -
- - - - Enable dark mode - -
+ Theme Mode + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + The theme mode of the extension. If you choose the system, will follow the system theme. + +
)} /> diff --git a/src/components/ui/RadioGroup.tsx b/src/components/ui/RadioGroup.tsx new file mode 100644 index 0000000..5178fb8 --- /dev/null +++ b/src/components/ui/RadioGroup.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { CheckIcon } from '@radix-ui/react-icons' +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' + +import { cn } from '@/utils/index' + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/src/domain/UserInfo.ts b/src/domain/UserInfo.ts index 1dc26da..a31671f 100644 --- a/src/domain/UserInfo.ts +++ b/src/domain/UserInfo.ts @@ -11,7 +11,7 @@ const UserInfoDomain = Remesh.domain({ USER_INFO_ID: 'USER_INFO_ID', USER_INFO_NAME: 'USER_INFO_NAME', USER_INFO_AVATAR: 'USER_INFO_AVATAR', - USER_INFO_DARK_MODE: 'USER_INFO_DARK_MODE' + USER_INFO_THEME_MODE: 'USER_INFO_THEME_MODE' } as const const UserInfoState = domain.state({ @@ -47,14 +47,14 @@ const UserInfoDomain = Remesh.domain({ id: from(storage.get(storageKeys.USER_INFO_ID)), name: from(storage.get(storageKeys.USER_INFO_NAME)), avatar: from(storage.get(storageKeys.USER_INFO_AVATAR)), - darkMode: from(storage.get(storageKeys.USER_INFO_DARK_MODE)) + themeMode: from(storage.get(storageKeys.USER_INFO_THEME_MODE)) }).pipe( map((userInfo) => { if ( !isEmpty(userInfo.id) && !isEmpty(userInfo.name) && !isEmpty(userInfo.avatar) && - !isEmpty(userInfo.darkMode) + !isEmpty(userInfo.themeMode) ) { return SetUserInfoCommand(userInfo as UserInfo) } else { @@ -74,7 +74,7 @@ const UserInfoDomain = Remesh.domain({ storage.set(storageKeys.USER_INFO_ID, userInfo?.id ?? null), storage.set(storageKeys.USER_INFO_NAME, userInfo?.name ?? null), storage.set(storageKeys.USER_INFO_AVATAR, userInfo?.avatar ?? null), - storage.set(storageKeys.USER_INFO_DARK_MODE, userInfo?.darkMode ?? null) + storage.set(storageKeys.USER_INFO_THEME_MODE, userInfo?.themeMode ?? null) ]) }) ) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index be7c69d..0cdf5fc 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -17,5 +17,5 @@ declare interface UserInfo { id: string name: string avatar: string - darkMode: boolean + themeMode: 'system' | 'light' | 'dark' } diff --git a/src/utils/checkSystemDarkMode.ts b/src/utils/checkSystemDarkMode.ts new file mode 100644 index 0000000..ac1f58a --- /dev/null +++ b/src/utils/checkSystemDarkMode.ts @@ -0,0 +1,3 @@ +const checkSystemDarkMode = () => window.matchMedia('(prefers-color-scheme: dark)').matches + +export default checkSystemDarkMode diff --git a/src/utils/index.ts b/src/utils/index.ts index 45b6364..28ba31a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,3 +6,4 @@ export { default as getSiteInfo } from './getSiteInfo' export { default as chunk } from './chunk' export { default as compressImage } from './compressImage' export { default as isEmpty } from './isEmpty' +export { default as checkSystemDarkMode } from './checkSystemDarkMode' diff --git a/src/utils/isEmpty.ts b/src/utils/isEmpty.ts index 67b20ec..f5b9897 100644 --- a/src/utils/isEmpty.ts +++ b/src/utils/isEmpty.ts @@ -1,6 +1,5 @@ -/** 检查是否是空值 */ const isEmpty = (value: any) => { - return value === undefined || value === null || value === '' + return value === undefined || value === null } export default isEmpty