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:
Vendored
+106
-117
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user