Feat: add theme switcher (#18093)

This commit is contained in:
KVOJJJin
2025-05-14 09:06:14 +08:00
committed by GitHub
parent 297d35364e
commit 3548c133e3
23 changed files with 330 additions and 54 deletions

View File

@@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon L">
<g id="Vector">
<path d="M2.66602 11.3333H0.666016L3.33268 8.66667L5.99935 11.3333H3.99935L3.99935 14H2.66602L2.66602 11.3333Z" fill="#354052"/>
<path d="M2.66602 4.66667L2.66602 2L3.99935 2L3.99935 4.66667L5.99935 4.66667L3.33268 7.33333L0.666016 4.66667L2.66602 4.66667Z" fill="#354052"/>
<path d="M7.33268 2.66667H13.9993V4H7.33268V2.66667ZM7.33268 12H13.9993V13.3333H7.33268V12ZM5.99935 7.33333H13.9993V8.66667H5.99935V7.33333Z" fill="#354052"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,62 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon L"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2.66602 11.3333H0.666016L3.33268 8.66667L5.99935 11.3333H3.99935L3.99935 14H2.66602L2.66602 11.3333Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2.66602 4.66667L2.66602 2L3.99935 2L3.99935 4.66667L5.99935 4.66667L3.33268 7.33333L0.666016 4.66667L2.66602 4.66667Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M7.33268 2.66667H13.9993V4H7.33268V2.66667ZM7.33268 12H13.9993V13.3333H7.33268V12ZM5.99935 7.33333H13.9993V8.66667H5.99935V7.33333Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Collapse"
}

View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Collapse.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Collapse'
export default Icon

View File

@@ -1,5 +1,6 @@
export { default as AlignLeft } from './AlignLeft'
export { default as BezierCurve03 } from './BezierCurve03'
export { default as Collapse } from './Collapse'
export { default as Colors } from './Colors'
export { default as ImageIndentLeft } from './ImageIndentLeft'
export { default as LeftIndent02 } from './LeftIndent02'

View File

@@ -0,0 +1,97 @@
'use client'
import { useState } from 'react'
import {
RiCheckLine,
RiComputerLine,
RiMoonLine,
RiSunLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useTheme } from 'next-themes'
import ActionButton from '@/app/components/base/action-button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
export type Theme = 'light' | 'dark' | 'system'
export default function ThemeSelector() {
const { t } = useTranslation()
const { theme, setTheme } = useTheme()
const [open, setOpen] = useState(false)
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme)
setOpen(false)
}
const getCurrentIcon = () => {
switch (theme) {
case 'light': return <RiSunLine className='h-4 w-4 text-text-tertiary' />
case 'dark': return <RiMoonLine className='h-4 w-4 text-text-tertiary' />
default: return <RiComputerLine className='h-4 w-4 text-text-tertiary' />
}
}
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{ mainAxis: 6 }}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
>
<ActionButton
className={`h-8 w-8 p-[6px] ${open && 'bg-state-base-hover'}`}
>
{getCurrentIcon()}
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className='flex w-[144px] flex-col items-start rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
<button
className='flex w-full items-center gap-1 rounded-lg px-2 py-1.5 text-text-secondary hover:bg-state-base-hover'
onClick={() => handleThemeChange('light')}
>
<RiSunLine className='h-4 w-4 text-text-tertiary' />
<div className='flex grow items-center justify-start px-1'>
<span className='system-md-regular'>{t('common.theme.light')}</span>
</div>
{theme === 'light' && <div className='flex h-4 w-4 shrink-0 items-center justify-center'>
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>}
</button>
<button
className='flex w-full items-center gap-1 rounded-lg px-2 py-1.5 text-text-secondary hover:bg-state-base-hover'
onClick={() => handleThemeChange('dark')}
>
<RiMoonLine className='h-4 w-4 text-text-tertiary' />
<div className='flex grow items-center justify-start px-1'>
<span className='system-md-regular'>{t('common.theme.dark')}</span>
</div>
{theme === 'dark' && <div className='flex h-4 w-4 shrink-0 items-center justify-center'>
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>}
</button>
<button
className='flex w-full items-center gap-1 rounded-lg px-2 py-1.5 text-text-secondary hover:bg-state-base-hover'
onClick={() => handleThemeChange('system')}
>
<RiComputerLine className='h-4 w-4 text-text-tertiary' />
<div className='flex grow items-center justify-start px-1'>
<span className='system-md-regular'>{t('common.theme.auto')}</span>
</div>
{theme === 'system' && <div className='flex h-4 w-4 shrink-0 items-center justify-center'>
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>}
</button>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

View File

@@ -0,0 +1,58 @@
'use client'
import {
RiComputerLine,
RiMoonLine,
RiSunLine,
} from '@remixicon/react'
import { useTheme } from 'next-themes'
import cn from '@/utils/classnames'
export type Theme = 'light' | 'dark' | 'system'
export default function ThemeSwitcher() {
const { theme, setTheme } = useTheme()
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme)
}
return (
<div className='flex items-center rounded-[10px] bg-components-segmented-control-bg-normal p-0.5'>
<div
className={cn(
'rounded-lg px-2 py-1 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
theme === 'system' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
)}
onClick={() => handleThemeChange('system')}
>
<div className='p-0.5'>
<RiComputerLine className='h-4 w-4' />
</div>
</div>
<div className={cn('h-[14px] w-px bg-transparent', theme === 'dark' && 'bg-divider-regular')}></div>
<div
className={cn(
'rounded-lg px-2 py-1 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
theme === 'light' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
)}
onClick={() => handleThemeChange('light')}
>
<div className='p-0.5'>
<RiSunLine className='h-4 w-4' />
</div>
</div>
<div className={cn('h-[14px] w-px bg-transparent', theme === 'system' && 'bg-divider-regular')}></div>
<div
className={cn(
'rounded-lg px-2 py-1 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
theme === 'dark' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
)}
onClick={() => handleThemeChange('dark')}
>
<div className='p-0.5'>
<RiMoonLine className='h-4 w-4' />
</div>
</div>
</div>
)
}