chore(form): theme switch use radio
This commit is contained in:
parent
92ba396ec0
commit
58527eae71
9 changed files with 115 additions and 25 deletions
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
|
|
36
src/components/ui/RadioGroup.tsx
Normal file
36
src/components/ui/RadioGroup.tsx
Normal 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 }
|
|
@ -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)
|
||||
])
|
||||
})
|
||||
)
|
||||
|
|
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
|
@ -17,5 +17,5 @@ declare interface UserInfo {
|
|||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
darkMode: boolean
|
||||
themeMode: 'system' | 'light' | 'dark'
|
||||
}
|
||||
|
|
3
src/utils/checkSystemDarkMode.ts
Normal file
3
src/utils/checkSystemDarkMode.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
const checkSystemDarkMode = () => window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
export default checkSystemDarkMode
|
|
@ -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'
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/** 检查是否是空值 */
|
||||
const isEmpty = (value: any) => {
|
||||
return value === undefined || value === null || value === ''
|
||||
return value === undefined || value === null
|
||||
}
|
||||
|
||||
export default isEmpty
|
||||
|
|
Loading…
Reference in a new issue