chore(form): theme switch use radio

This commit is contained in:
molvqingtai 2023-12-02 01:14:31 +08:00
parent 92ba396ec0
commit 58527eae71
9 changed files with 115 additions and 25 deletions

View file

@ -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",

View file

@ -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:

View file

@ -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 = () => {
/>
<FormField
control={form.control}
name="darkMode"
name="themeMode"
render={({ field }) => (
<FormItem>
<FormLabel>DarkMode</FormLabel>
<div className="flex items-center gap-x-2">
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange}></Switch>
</FormControl>
<FormDescription>Enable dark mode</FormDescription>
<FormMessage />
</div>
<FormLabel>Theme Mode</FormLabel>
<FormControl>
<RadioGroup className="flex gap-x-4" onValueChange={field.onChange} value={field.value}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="system" id="r1" />
<Label htmlFor="r1">System</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="light" id="r2" />
<Label htmlFor="r2">Light</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="dark" id="r3" />
<Label htmlFor="r3">Dark</Label>
</div>
</RadioGroup>
</FormControl>
<FormDescription>
The theme mode of the extension. If you choose the system, will follow the system theme.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

View file

@ -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<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View file

@ -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<UserInfo | null>({
@ -47,14 +47,14 @@ const UserInfoDomain = Remesh.domain({
id: from(storage.get<UserInfo['id']>(storageKeys.USER_INFO_ID)),
name: from(storage.get<UserInfo['name']>(storageKeys.USER_INFO_NAME)),
avatar: from(storage.get<UserInfo['avatar']>(storageKeys.USER_INFO_AVATAR)),
darkMode: from(storage.get<UserInfo['darkMode']>(storageKeys.USER_INFO_DARK_MODE))
themeMode: from(storage.get<UserInfo['themeMode']>(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<UserInfo['id'] | null>(storageKeys.USER_INFO_ID, userInfo?.id ?? null),
storage.set<UserInfo['name'] | null>(storageKeys.USER_INFO_NAME, userInfo?.name ?? null),
storage.set<UserInfo['avatar'] | null>(storageKeys.USER_INFO_AVATAR, userInfo?.avatar ?? null),
storage.set<UserInfo['darkMode'] | null>(storageKeys.USER_INFO_DARK_MODE, userInfo?.darkMode ?? null)
storage.set<UserInfo['themeMode'] | null>(storageKeys.USER_INFO_THEME_MODE, userInfo?.themeMode ?? null)
])
})
)

View file

@ -17,5 +17,5 @@ declare interface UserInfo {
id: string
name: string
avatar: string
darkMode: boolean
themeMode: 'system' | 'light' | 'dark'
}

View file

@ -0,0 +1,3 @@
const checkSystemDarkMode = () => window.matchMedia('(prefers-color-scheme: dark)').matches
export default checkSystemDarkMode

View file

@ -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'

View file

@ -1,6 +1,5 @@
/** 检查是否是空值 */
const isEmpty = (value: any) => {
return value === undefined || value === null || value === ''
return value === undefined || value === null
}
export default isEmpty