Refine mobile menu UI/UX
- Convert nav links to block elements with padding - Improve Login/Register button layout - Update LanguageToggle and ReleaseChannelSelector to use semantic tokens Co-authored-by: cloud-neutral <4133689+cloud-neutral@users.noreply.github.com>
This commit is contained in:
parent
bf2e5ec341
commit
fb3e914a95
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@ -100,4 +100,4 @@
|
||||
"glob": "10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0"
|
||||
}
|
||||
}
|
||||
@ -51,6 +51,7 @@ export function AskAIDialog({
|
||||
new Map<string, { answer: string; sources: any[]; timestamp: number }>()
|
||||
)
|
||||
const requestIdRef = useRef(0)
|
||||
const requestIdRef = useRef(0)
|
||||
const processedInitialRef = useRef<number | null>(null)
|
||||
|
||||
const { language } = useLanguage()
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
'use client'
|
||||
import { useLanguage } from '../i18n/LanguageProvider'
|
||||
|
||||
export default function LanguageToggle() {
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function LanguageToggle({ className = '' }: Props) {
|
||||
const { language, setLanguage } = useLanguage()
|
||||
|
||||
return (
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value as 'en' | 'zh')}
|
||||
className="bg-gray-100 text-gray-900 border border-gray-300 px-2 py-1 rounded text-sm"
|
||||
className={`rounded border border-surface-border bg-surface px-2 py-1 text-sm text-text ${className}`}
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="zh">中文</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
// This component provides a dropdown to toggle between English and Chinese languages.
|
||||
@ -387,12 +387,12 @@ export default function Navbar() {
|
||||
inputClassName="py-2 pr-12"
|
||||
/>
|
||||
*/}
|
||||
<div className="flex flex-col gap-2 text-sm font-medium">
|
||||
<div className="flex flex-col gap-1 text-sm font-medium">
|
||||
{mainLinks.map((link) => (
|
||||
<Link
|
||||
key={link.key}
|
||||
href={link.href}
|
||||
className="py-2 text-sm opacity-80 transition hover:opacity-100"
|
||||
className="block rounded-md px-3 py-3 text-base font-medium text-text opacity-80 transition-colors hover:bg-surface-muted hover:text-primary hover:opacity-100"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
@ -400,7 +400,7 @@ export default function Navbar() {
|
||||
))}
|
||||
<Link
|
||||
href="/about"
|
||||
className="py-2 text-sm opacity-80 transition hover:opacity-100"
|
||||
className="block rounded-md px-3 py-3 text-base font-medium text-text opacity-80 transition-colors hover:bg-surface-muted hover:text-primary hover:opacity-100"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{labels.about}
|
||||
@ -408,7 +408,7 @@ export default function Navbar() {
|
||||
<Link
|
||||
key={servicesLink.key}
|
||||
href={servicesLink.href}
|
||||
className="py-2 text-sm opacity-80 transition hover:opacity-100"
|
||||
className="block rounded-md px-3 py-3 text-base font-medium text-text opacity-80 transition-colors hover:bg-surface-muted hover:text-primary hover:opacity-100"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{servicesLink.label}
|
||||
@ -425,41 +425,42 @@ export default function Navbar() {
|
||||
<p className="text-xs text-text-muted">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/panel"
|
||||
className="mt-3 inline-flex items-center justify-center rounded-md border border-surface-border bg-surface px-3 py-1.5 text-xs font-semibold text-primary transition hover:border-primary/50 hover:bg-primary/10"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{accountCopy.userCenter}
|
||||
</Link>
|
||||
<Link
|
||||
href="/logout"
|
||||
className="mt-3 inline-flex items-center justify-center rounded-md border border-surface-border px-3 py-1.5 text-xs font-semibold text-danger transition hover:border-danger/60 hover:bg-danger/10 focus:outline-none focus:ring-2 focus:ring-danger/30 focus:ring-offset-2 focus:ring-offset-background"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{accountCopy.logout}
|
||||
</Link>
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<Link
|
||||
href="/panel"
|
||||
className="flex w-full items-center justify-center rounded-lg border border-surface-border bg-surface px-4 py-2.5 text-sm font-medium text-primary transition hover:border-primary/50 hover:bg-primary/10"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{accountCopy.userCenter}
|
||||
</Link>
|
||||
<Link
|
||||
href="/logout"
|
||||
className="flex w-full items-center justify-center rounded-lg border border-surface-border px-4 py-2.5 text-sm font-medium text-danger transition hover:border-danger/60 hover:bg-danger/10 focus:outline-none focus:ring-2 focus:ring-danger/30 focus:ring-offset-2 focus:ring-offset-background"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{accountCopy.logout}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 text-sm font-medium">
|
||||
<div className="mt-2 grid grid-cols-2 gap-4 text-sm font-medium">
|
||||
<Link
|
||||
href="/login"
|
||||
className="py-2 text-sm opacity-80 transition hover:opacity-100"
|
||||
className="flex w-full items-center justify-center rounded-lg border border-surface-border bg-surface px-4 py-2.5 text-sm font-medium text-text transition hover:bg-surface-muted"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{nav.account.login}
|
||||
</Link>
|
||||
<span className="h-3 w-px bg-surface-border" aria-hidden="true" />
|
||||
<Link
|
||||
href="/register"
|
||||
className="rounded-md border border-surface-border px-3 py-1.5 text-primary transition hover:border-primary/50 hover:bg-surface-muted"
|
||||
className="flex w-full items-center justify-center rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-white shadow-sm transition hover:bg-primary/90"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{nav.account.register}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<ReleaseChannelSelector selected={selectedChannels} onToggle={toggleChannel} />
|
||||
<LanguageToggle />
|
||||
</div>
|
||||
|
||||
@ -53,7 +53,7 @@ export default function ReleaseChannelSelector({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className={`relative flex items-center gap-2 rounded-md border border-gray-200 bg-white/80 text-sm text-gray-700 shadow-sm transition hover:border-purple-300 hover:text-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
||||
className={`relative flex items-center gap-2 rounded-md border border-surface-border bg-surface text-sm text-text shadow-sm transition hover:border-primary/50 hover:text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${
|
||||
isIcon
|
||||
? 'h-9 w-9 justify-center p-0'
|
||||
: isCompact
|
||||
@ -66,7 +66,7 @@ export default function ReleaseChannelSelector({
|
||||
>
|
||||
{isIcon ? (
|
||||
<svg
|
||||
className="h-5 w-5 text-purple-600"
|
||||
className="h-5 w-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@ -86,14 +86,14 @@ export default function ReleaseChannelSelector({
|
||||
</svg>
|
||||
) : (
|
||||
<>
|
||||
<span className={`font-medium ${isCompact ? '' : 'text-gray-700'}`}>{labels.label}</span>
|
||||
<span className={`font-medium ${isCompact ? '' : 'text-text'}`}>{labels.label}</span>
|
||||
{!isCompact && (
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="text-xs text-text-muted">
|
||||
{labels.summaryPrefix}: {summary}
|
||||
</span>
|
||||
)}
|
||||
<svg
|
||||
className={`h-4 w-4 text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
className={`h-4 w-4 text-text-subtle transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@ -105,37 +105,37 @@ export default function ReleaseChannelSelector({
|
||||
</>
|
||||
)}
|
||||
{isIcon && hasPreviewSelection && (
|
||||
<span className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-purple-500" />
|
||||
<span className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
{(isCompact || isIcon) && (
|
||||
<div className="pointer-events-none absolute bottom-full left-1/2 z-40 mb-2 hidden w-56 -translate-x-1/2 rounded-md bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition group-hover:block group-hover:opacity-100 group-focus-within:block group-focus-within:opacity-100">
|
||||
<div className="pointer-events-none absolute bottom-full left-1/2 z-40 mb-2 hidden w-56 -translate-x-1/2 rounded-md bg-surface-inverted px-3 py-2 text-xs text-background opacity-0 transition group-hover:block group-hover:opacity-100 group-focus-within:block group-focus-within:opacity-100">
|
||||
<div className="font-semibold">{labels.label}</div>
|
||||
<div className="mt-1 text-gray-200">
|
||||
<div className="mt-1 text-background-muted">
|
||||
{labels.summaryPrefix}: {summary}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{open && (
|
||||
<div className="absolute right-0 z-50 mt-2 w-64 rounded-md border border-gray-200 bg-white shadow-lg">
|
||||
<ul className="py-2 text-sm text-gray-700" role="listbox" aria-label={labels.label}>
|
||||
<div className="absolute right-0 z-50 mt-2 w-64 rounded-md border border-surface-border bg-surface shadow-lg">
|
||||
<ul className="py-2 text-sm text-text" role="listbox" aria-label={labels.label}>
|
||||
{CHANNEL_ORDER.map((channel) => {
|
||||
const channelLabels = labels[channel]
|
||||
const checked = selected.includes(channel)
|
||||
const isStable = channel === 'stable'
|
||||
return (
|
||||
<li key={channel}>
|
||||
<label className="flex cursor-pointer items-start gap-3 px-3 py-2 hover:bg-gray-50">
|
||||
<label className="flex cursor-pointer items-start gap-3 px-3 py-2 hover:bg-surface-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||
className="mt-1 h-4 w-4 rounded border-surface-border text-primary focus:ring-primary"
|
||||
checked={checked}
|
||||
onChange={() => (!isStable ? onToggle(channel) : undefined)}
|
||||
disabled={isStable}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{channelLabels.name}</div>
|
||||
<p className="text-xs text-gray-500">{channelLabels.description}</p>
|
||||
<div className="font-medium text-text">{channelLabels.name}</div>
|
||||
<p className="text-xs text-text-muted">{channelLabels.description}</p>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user