1+ import { getThemeSwitchMeta } from '@/conf/themeSwitch.manifest'
12import { useGlobal } from '@/lib/global'
23import { getQueryParam } from '@/lib/utils'
34import { THEMES } from '@/themes/theme'
45import { useRouter } from 'next/router'
56import { useState } from 'react'
6- import DarkModeButton from './DarkModeButton'
77import { Draggable } from './Draggable'
8+ import { Moon , Sun } from './HeroIcons'
89import LazyImage from './LazyImage'
910import SideBarDrawer from './SideBarDrawer'
11+
12+ function ThemeTierBadge ( { tier, labels } ) {
13+ const isPaid = tier === 'paid'
14+ const text = isPaid ? labels . paid : labels . free
15+ const cls = isPaid
16+ ? 'border-amber-200/90 bg-amber-50 text-amber-800 dark:border-amber-500/40 dark:bg-amber-950/45 dark:text-amber-200'
17+ : 'border-emerald-200/90 bg-emerald-50 text-emerald-800 dark:border-emerald-500/35 dark:bg-emerald-950/40 dark:text-emerald-200'
18+ return (
19+ < span
20+ className = { `inline-flex max-w-full shrink-0 items-center truncate rounded-md border px-1.5 py-0.5 text-[10px] font-semibold tracking-wide ${ cls } ` }
21+ title = { text } >
22+ { text }
23+ </ span >
24+ )
25+ }
26+
1027/**
1128 *
1229 * @returns 主题切换
@@ -17,6 +34,12 @@ const ThemeSwitch = () => {
1734 const currentTheme = getQueryParam ( router . asPath , 'theme' ) || theme
1835 const [ sideBarVisible , setSideBarVisible ] = useState ( false )
1936
37+ const currentMeta = getThemeSwitchMeta ( currentTheme )
38+ const tierLabels = {
39+ free : locale . COMMON ?. THEME_TIER_FREE ?? 'Free' ,
40+ paid : locale . COMMON ?. THEME_TIER_PAID ?? 'Paid'
41+ }
42+
2043 const changeTheme = newTheme => {
2144 const query = router . query
2245 query . theme = newTheme
@@ -30,94 +53,168 @@ const ThemeSwitch = () => {
3053 < div
3154 id = 'draggableBox'
3255 style = { { left : '0px' , top : '80vh' } }
33- className = 'border dark:border-gray-600 fixed group flex flex-col items-start space-y-2 overflow-hidden z-20 p-3
34- dark:text-white bg-white dark:bg-black
35- rounded-xl shadow-lg hover:scale-105 hover:shadow-2xl ' >
36- { /* 主题切换按钮 */ }
37- < div className = 'text-sm flex items-center group-hover:w-44 h-4 text-center duration-200' >
38- < i
39- className = 'cursor-pointer fa-solid fa-palette w-5 '
56+ className = 'group fixed z-50 flex max-w-[min(100vw-1rem,16rem)] select-none flex-col items-stretch overflow-hidden rounded-2xl border border-gray-200/80 bg-white/90 p-1.5 shadow-[0_8px_30px_rgb(0,0,0,0.12)] ring-1 ring-black/5 backdrop-blur-md transition hover:border-gray-300/90 hover:shadow-[0_12px_40px_rgb(0,0,0,0.15)] dark:border-gray-600/70 dark:bg-gray-950/92 dark:ring-white/10 dark:hover:border-gray-500/80' >
57+ { /* 悬浮入口:仅样式优化,点击逻辑不变 */ }
58+ < div className = 'flex min-h-[3rem] items-center gap-2.5 pl-0.5 pr-1 duration-200 group-hover:gap-3' >
59+ < span
60+ className = 'relative flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-xl bg-gradient-to-br from-indigo-500 via-violet-600 to-fuchsia-600 text-white shadow-md ring-2 ring-white/30 transition active:scale-95 dark:from-indigo-400 dark:via-violet-500 dark:to-fuchsia-500 dark:ring-white/20'
4061 onClick = { ( ) => {
4162 setSideBarVisible ( true )
4263 } }
4364 onTouchStart = { ( ) => {
4465 setSideBarVisible ( true )
4566 } }
46- />
47- < div className = 'w-0 group-hover:w-32 duration-200 overflow-hidden' >
67+ title = 'Open theme panel' >
68+ < span className = 'absolute inset-0 rounded-xl bg-gradient-to-t from-black/10 to-transparent' />
69+ < i className = 'fa-solid fa-palette relative text-[15px]' aria-hidden />
70+ </ span >
71+ < div className = 'min-w-0 flex-1 overflow-hidden py-0.5' >
4872 < label htmlFor = 'themeSelect' className = 'sr-only' >
4973 { locale . COMMON . THEME }
5074 </ label >
51- { /* 点击弹出主题切换面板 */ }
52- < div
75+ < button
76+ id = 'themeSelect'
77+ type = 'button'
78+ className = 'w-full cursor-pointer text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/80 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-gray-950'
5379 onClick = { ( ) => {
5480 setSideBarVisible ( true )
5581 } }
56- className = 'uppercase cursor-pointer'
57- title = 'Click To Switch Theme'
58- alt = 'Click To Switch Theme' >
59- { currentTheme }
60- </ div >
82+ title = 'Click To Switch Theme' >
83+ < span className = 'block text-[10px] font-semibold uppercase tracking-[0.14em] text-gray-500 dark:text-gray-400' >
84+ { locale . MENU . THEME_SWITCH }
85+ </ span >
86+ < span className = 'mt-0.5 flex min-w-0 items-center gap-1.5' >
87+ < span className = 'min-w-0 flex-1 truncate text-sm font-semibold leading-snug text-gray-900 dark:text-gray-50' >
88+ { currentMeta . name }
89+ </ span >
90+ < ThemeTierBadge tier = { currentMeta . tier } labels = { tierLabels } />
91+ </ span >
92+ </ button >
6193 </ div >
6294 </ div >
6395 </ div >
6496 </ Draggable >
6597
6698 < SideBarDrawer
67- className = 'p-10 max-w-3xl 2xl:max-w-5xl dark:text-white bg-white dark:bg-black '
99+ className = 'flex max-h-screen w-[min(100vw-0.5rem,28rem)] flex-col overflow-hidden border-r border-gray-200/90 bg-white p-0 shadow-2xl dark:border-gray-800 dark: bg-gray-950 md:w-[min(100vw-2rem,48rem)] lg:w-[min(100vw-3rem,56rem)] '
68100 isOpen = { sideBarVisible }
69101 showOnPC = { true }
70102 onClose = { ( ) => {
71103 setSideBarVisible ( false )
72104 } } >
73- { /* 开关 */ }
74- < div className = 'flex items-center justify-between font-bold' >
75- { /* 深色模式切换 */ }
76- < div className = 'border dark:border-gray-60 text-sm flex items-center w-32 duration-200 hover:bg-green-500 p-2' >
77- < DarkModeButton />
78- < div
79- onClick = { toggleDarkMode }
80- className = 'cursor-pointer w-24 duration-200 overflow-hidden whitespace-nowrap pl-1 h-auto' >
81- { isDarkMode ? locale . MENU . DARK_MODE : locale . MENU . LIGHT_MODE }
105+ < div className = 'flex min-h-0 flex-1 flex-col overflow-y-auto' >
106+ < div className = 'sticky top-0 z-[1] border-b border-gray-100 bg-white/95 px-5 py-4 backdrop-blur-md dark:border-gray-800 dark:bg-gray-950/95' >
107+ < div className = 'flex items-start justify-between gap-3' >
108+ < div className = 'min-w-0' >
109+ < p className = 'text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500' >
110+ { locale . MENU . THEME_SWITCH }
111+ </ p >
112+ < p className = 'mt-0.5 truncate text-sm text-gray-600 dark:text-gray-300' >
113+ { locale . COMMON . THEME_SWITCH }
114+ </ p >
115+ </ div >
116+ < button
117+ type = 'button'
118+ className = 'flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-gray-500 transition hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-white'
119+ onClick = { ( ) => {
120+ setSideBarVisible ( false )
121+ } } >
122+ < i className = 'fas fa-times' />
123+ </ button >
82124 </ div >
83125 </ div >
84126
85- { /* 关闭 */ }
86- < div
87- className = 'hover:bg-green-500 px-2 py-1 duration-200 cursor-pointer'
88- onClick = { ( ) => {
89- setSideBarVisible ( false )
90- } } >
91- < i className = 'fas fa-times' />
92- </ div >
93- </ div >
94-
95- < hr className = 'my-4 dark:border-gray-600' />
127+ < div className = 'space-y-6 px-5 pb-10 pt-6' >
128+ { /* 明暗切换:整行可点,仍为 toggleDarkMode */ }
129+ < button
130+ type = 'button'
131+ onClick = { toggleDarkMode }
132+ aria-label = {
133+ isDarkMode ? '切换为浅色模式' : '切换为深色模式'
134+ }
135+ className = 'flex w-full items-center justify-between gap-3 rounded-2xl border-2 border-indigo-200/90 bg-gradient-to-r from-sky-50 via-white to-violet-50 px-4 py-3.5 text-left shadow-sm ring-1 ring-indigo-100/80 transition hover:border-indigo-400 hover:shadow-md active:scale-[0.99] dark:border-indigo-500/40 dark:from-gray-900 dark:via-gray-900 dark:to-indigo-950/60 dark:ring-indigo-500/20 dark:hover:border-indigo-400/70' >
136+ < span className = 'flex min-w-0 flex-1 items-center gap-3' >
137+ < span className = 'flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-white text-indigo-600 shadow-md ring-1 ring-gray-200 dark:bg-gray-800 dark:text-amber-300 dark:ring-gray-600 [&_svg]:h-7 [&_svg]:w-7' >
138+ { isDarkMode ? < Sun /> : < Moon /> }
139+ </ span >
140+ < span className = 'min-w-0' >
141+ < span className = 'block text-sm font-semibold text-gray-900 dark:text-white' >
142+ { isDarkMode ? locale . MENU . DARK_MODE : locale . MENU . LIGHT_MODE }
143+ </ span >
144+ < span className = 'mt-0.5 block text-xs text-gray-600 dark:text-gray-400' >
145+ { isDarkMode
146+ ? '点击切换为浅色模式'
147+ : '点击切换为深色模式' }
148+ </ span >
149+ </ span >
150+ </ span >
151+ < span
152+ className = { `relative inline-flex h-9 w-[3.35rem] shrink-0 items-center rounded-full border border-transparent shadow-inner transition-colors ${
153+ isDarkMode ? 'bg-indigo-600' : 'bg-gray-300 dark:bg-gray-600'
154+ } `}
155+ aria-hidden >
156+ < span
157+ className = { `pointer-events-none absolute top-1 left-1 h-7 w-7 rounded-full bg-white shadow ring-1 ring-black/10 transition-transform duration-200 ease-out ${
158+ isDarkMode ? 'translate-x-[1.35rem]' : 'translate-x-0'
159+ } `}
160+ />
161+ </ span >
162+ </ button >
96163
97- < div > 点击下方主题进行切换.</ div >
98- < div > Click below to switch the theme.</ div >
164+ < div >
165+ < p className = 'text-sm font-medium text-gray-900 dark:text-gray-100' >
166+ 点击下方主题进行切换.
167+ </ p >
168+ < p className = 'mt-1 text-xs text-gray-500 dark:text-gray-400' >
169+ Click below to switch the theme.
170+ </ p >
171+ </ div >
99172
100- { /* 陈列所有主题 */ }
101- < div className = 'grid lg:grid-cols-2 gap-6' >
102- { THEMES ?. map ( t => {
103- return (
104- < div
105- className = 'my-6'
106- key = { t }
107- onClick = { ( ) => {
108- changeTheme ( t )
109- } } >
110- < div className = 'text-lg dark:text-white font-bold uppercase mb-4' >
111- { t }
112- </ div >
113- < LazyImage
114- src = { `/images/themes-preview/${ t } .webp` }
115- fallbackSrc = { `/images/themes-preview/${ t } .png` }
116- className = 'cursor-pointer shadow-lg rounded-xl hover:scale-110 duration-200'
117- />
118- </ div >
119- )
120- } ) }
173+ { /* 陈列所有主题 — 仍为同一 map + changeTheme(t) */ }
174+ < div className = 'grid grid-cols-1 gap-5 sm:grid-cols-2' >
175+ { THEMES ?. map ( t => {
176+ const active = currentTheme === t
177+ const meta = getThemeSwitchMeta ( t )
178+ return (
179+ < div
180+ className = { `cursor-pointer rounded-2xl border bg-white p-1.5 shadow-sm transition dark:bg-gray-900/40 ${
181+ active
182+ ? 'border-indigo-500 ring-2 ring-indigo-500/30 dark:border-indigo-400'
183+ : 'border-gray-100 hover:border-indigo-300 hover:shadow-md dark:border-gray-800 dark:hover:border-indigo-500/40'
184+ } `}
185+ key = { t }
186+ onClick = { ( ) => {
187+ changeTheme ( t )
188+ } } >
189+ < div className = 'mb-1 flex flex-wrap items-center justify-center gap-1.5 px-1 text-center' >
190+ < span className = 'min-w-0 truncate text-xs font-semibold tracking-wide text-gray-800 dark:text-gray-100' >
191+ { meta . name }
192+ </ span >
193+ < ThemeTierBadge tier = { meta . tier } labels = { tierLabels } />
194+ </ div >
195+ { meta . summary ? (
196+ < p className = 'mb-2 line-clamp-2 px-1 text-center text-[11px] leading-snug text-gray-500 dark:text-gray-400' >
197+ { meta . summary }
198+ </ p >
199+ ) : null }
200+ < div className = 'relative overflow-hidden rounded-xl' >
201+ < LazyImage
202+ src = { meta . coverWebp || meta . coverPng }
203+ fallbackSrc = { meta . coverPng }
204+ alt = { `${ meta . name } preview` }
205+ className = 'w-full cursor-pointer rounded-xl object-cover shadow-inner transition duration-300 hover:scale-[1.02]'
206+ />
207+ { active && (
208+ < span className = 'absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-full bg-indigo-600 text-xs text-white shadow-lg' >
209+ < i className = 'fas fa-check' />
210+ </ span >
211+ ) }
212+ </ div >
213+ </ div >
214+ )
215+ } ) }
216+ </ div >
217+ </ div >
121218 </ div >
122219 </ SideBarDrawer >
123220 </ >
0 commit comments