perf(web): improve dialog sizing and footer layout

- migrate frontend dialogs to the shared footer API so actions stay separated from scrollable body content.
- tune dialog dimensions for model analytics, prefill groups, billing history, channel model sync, and related workflows.
- update channel terminology and dialog action translations across supported locales.
This commit is contained in:
QuentinHsu
2026-06-06 21:49:33 +08:00
parent 7a5348caa3
commit 2eaa943d9f
80 changed files with 8137 additions and 8517 deletions
@@ -20,16 +20,9 @@ import { useMemo } from 'react'
import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Dialog } from '@/components/dialog'
import type {
SecureVerificationState,
VerificationMethod,
@@ -91,122 +84,118 @@ export function SecureVerificationDialog({
(activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6))
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 gap-0 overflow-hidden border-none p-0 shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl'
showCloseButton={!state.loading}
>
<div className='bg-background flex max-h-[calc(100dvh-2rem)] flex-col'>
<DialogHeader className='border-b px-6 py-5 text-left'>
<DialogTitle className='flex items-center gap-2 text-lg font-semibold'>
<ShieldCheck className='text-primary h-5 w-5' />
{title}
</DialogTitle>
<DialogDescription className='text-left'>
{description}
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={
<>
<ShieldCheck className='text-primary h-5 w-5' />
{title}
</>
}
description={description}
contentClassName='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 overflow-hidden border-none shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl'
headerClassName='border-b pb-4 text-left'
titleClassName='flex items-center gap-2 text-lg font-semibold'
descriptionClassName='text-left'
contentHeight='auto'
bodyClassName='px-1 py-1'
showCloseButton={!state.loading}
footerClassName='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end'
footer={
<>
<Button
type='button'
variant='outline'
disabled={state.loading}
onClick={onCancel}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleVerify}
disabled={availableTabs.length === 0 || verifyDisabled}
>
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</>
}
>
{availableTabs.length === 0 ? (
<div className='grid place-items-center gap-4 text-center'>
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
<ShieldCheck className='text-muted-foreground h-8 w-8' />
</div>
<p className='text-muted-foreground text-sm'>
{t(
'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.'
)}
</p>
</div>
) : (
<Tabs
value={activeMethod ?? availableTabs[0]}
onValueChange={(value) => onMethodChange(value as VerificationMethod)}
className='gap-4'
>
<TabsList>
{methods.has2FA && (
<TabsTrigger value='2fa'>{t('Authenticator code')}</TabsTrigger>
)}
{methods.hasPasskey && methods.passkeySupported && (
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
)}
</TabsList>
<div className='flex-1 overflow-y-auto px-6 py-5'>
{availableTabs.length === 0 ? (
<div className='grid place-items-center gap-4 text-center'>
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
<ShieldCheck className='text-muted-foreground h-8 w-8' />
</div>
<p className='text-muted-foreground text-sm'>
{t(
'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.'
)}
</p>
</div>
) : (
<Tabs
value={activeMethod ?? availableTabs[0]}
onValueChange={(value) =>
onMethodChange(value as VerificationMethod)
<TabsContent value='2fa' className='space-y-3'>
<p className='text-muted-foreground text-sm'>
{t(
'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.'
)}
</p>
<Input
inputMode='numeric'
maxLength={8}
value={state.code}
onChange={(event) => onCodeChange(event.target.value)}
placeholder={t('Enter verification code')}
disabled={state.loading}
autoFocus={activeMethod === '2fa'}
onKeyDown={(event) => {
if (event.key === 'Enter' && !verifyDisabled) {
event.preventDefault()
handleVerify()
}
className='gap-4'
>
<TabsList>
{methods.has2FA && (
<TabsTrigger value='2fa'>
{t('Authenticator code')}
</TabsTrigger>
)}
{methods.hasPasskey && methods.passkeySupported && (
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
)}
</TabsList>
}}
/>
</TabsContent>
<TabsContent value='2fa' className='space-y-3'>
<p className='text-muted-foreground text-sm'>
<TabsContent value='passkey' className='space-y-4'>
<div className='bg-muted/50 flex items-center justify-center rounded-lg p-4'>
<div className='text-muted-foreground flex items-center gap-3'>
<KeyRound className='text-primary h-6 w-6' />
<div className='text-left text-sm'>
<p className='text-foreground font-medium'>
{t('Use your Passkey')}
</p>
<p>
{t(
'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.'
'We will prompt your device to confirm using biometrics or your hardware key.'
)}
</p>
<Input
inputMode='numeric'
maxLength={8}
value={state.code}
onChange={(event) => onCodeChange(event.target.value)}
placeholder={t('Enter verification code')}
disabled={state.loading}
autoFocus={activeMethod === '2fa'}
onKeyDown={(event) => {
if (event.key === 'Enter' && !verifyDisabled) {
event.preventDefault()
handleVerify()
}
}}
/>
</TabsContent>
<TabsContent value='passkey' className='space-y-4'>
<div className='bg-muted/50 flex items-center justify-center rounded-lg p-4'>
<div className='text-muted-foreground flex items-center gap-3'>
<KeyRound className='text-primary h-6 w-6' />
<div className='text-left text-sm'>
<p className='text-foreground font-medium'>
{t('Use your Passkey')}
</p>
<p>
{t(
'We will prompt your device to confirm using biometrics or your hardware key.'
)}
</p>
</div>
</div>
</div>
{!methods.passkeySupported && (
<p className='text-destructive text-sm'>
{t('This device does not support Passkey verification.')}
</p>
)}
</TabsContent>
</Tabs>
</div>
</div>
</div>
{!methods.passkeySupported && (
<p className='text-destructive text-sm'>
{t('This device does not support Passkey verification.')}
</p>
)}
</div>
<DialogFooter className='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end'>
<Button
type='button'
variant='outline'
disabled={state.loading}
onClick={onCancel}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleVerify}
disabled={availableTabs.length === 0 || verifyDisabled}
>
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</DialogFooter>
</div>
</DialogContent>
</TabsContent>
</Tabs>
)}
</Dialog>
)
}
@@ -32,14 +32,6 @@ import {
import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
@@ -50,6 +42,7 @@ import {
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile'
import { login, wechatLoginByCode } from '@/features/auth/api'
@@ -414,43 +407,16 @@ export function UserAuthForm({
<Dialog
open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange}
>
<DialogContent className='max-w-sm'>
<DialogHeader className='text-left'>
<DialogTitle>{t('WeChat sign in')}</DialogTitle>
<DialogDescription>
{t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
</DialogDescription>
</DialogHeader>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
title={t('WeChat sign in')}
description={t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
contentClassName='max-w-sm'
headerClassName='text-left'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
@@ -474,8 +440,32 @@ export function UserAuthForm({
) : null}
{t('Confirm')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog>
)}
</Form>
@@ -26,14 +26,6 @@ import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
@@ -44,6 +36,7 @@ import {
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile'
import { register, wechatLoginByCode } from '@/features/auth/api'
@@ -387,43 +380,16 @@ export function SignUpForm({
<Dialog
open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange}
>
<DialogContent className='max-w-sm'>
<DialogHeader className='text-left'>
<DialogTitle>{t('WeChat sign in')}</DialogTitle>
<DialogDescription>
{t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
</DialogDescription>
</DialogHeader>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
title={t('WeChat sign in')}
description={t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
contentClassName='max-w-sm'
headerClassName='text-left'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
@@ -447,8 +413,32 @@ export function SignUpForm({
) : null}
{t('Confirm')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog>
)}
</Form>