Skip to content

Commit 4e89274

Browse files
authored
Merge pull request notionnext-org#4013 from tangly1024/fix/contact-email-themes-and-docs
feat(theme-switch): manifest, tier badges, wider panel, docs
2 parents d59e54d + 1c26268 commit 4e89274

27 files changed

Lines changed: 630 additions & 120 deletions

File tree

components/SideBarDrawer.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useRouter } from 'next/router'
2-
import { useEffect } from 'react'
2+
import { useEffect, useState } from 'react'
33

44
/**
55
* 侧边栏抽屉面板,可以从侧面拉出
@@ -16,6 +16,22 @@ const SideBarDrawer = ({
1616
}) => {
1717
const router = useRouter()
1818

19+
/**
20+
* 移动端:打开抽屉后同一手势会触发「幽灵点击」落在全屏遮罩上导致立刻关闭。
21+
* 打开后短时间内遮罩 pointer-events: none,超时后再响应关闭。
22+
*/
23+
const [backdropInteractive, setBackdropInteractive] = useState(false)
24+
25+
useEffect(() => {
26+
if (!isOpen) {
27+
setBackdropInteractive(false)
28+
return
29+
}
30+
setBackdropInteractive(false)
31+
const id = window.setTimeout(() => setBackdropInteractive(true), 450)
32+
return () => window.clearTimeout(id)
33+
}, [isOpen])
34+
1935
useEffect(() => {
2036
const sideBarDrawerRouteListener = () => {
2137
switchSideDrawerVisible(false)
@@ -60,8 +76,14 @@ const SideBarDrawer = ({
6076
{/* 背景蒙版 */}
6177
<div
6278
id='sidebar-drawer-background'
63-
onClick={() => switchSideDrawerVisible(false)}
64-
className={`${isOpen ? 'block' : 'hidden'} fixed top-0 left-0 z-20 w-full h-full bg-black/70 transition-opacity duration-300`}
79+
role='presentation'
80+
onClick={() => {
81+
if (!backdropInteractive) return
82+
switchSideDrawerVisible(false)
83+
}}
84+
className={`${isOpen ? 'block' : 'hidden'} fixed top-0 left-0 z-20 w-full h-full bg-black/70 transition-opacity duration-300 ${
85+
isOpen && !backdropInteractive ? 'pointer-events-none' : ''
86+
}`}
6587
/>
6688
</div>
6789
)

components/ThemeSwitch.js

Lines changed: 158 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
1+
import { getThemeSwitchMeta } from '@/conf/themeSwitch.manifest'
12
import { useGlobal } from '@/lib/global'
23
import { getQueryParam } from '@/lib/utils'
34
import { THEMES } from '@/themes/theme'
45
import { useRouter } from 'next/router'
56
import { useState } from 'react'
6-
import DarkModeButton from './DarkModeButton'
77
import { Draggable } from './Draggable'
8+
import { Moon, Sun } from './HeroIcons'
89
import LazyImage from './LazyImage'
910
import 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

Comments
 (0)