feat(ui): improve mobile responsive layouts

This commit is contained in:
CaIon
2026-04-30 19:53:02 +08:00
parent aa730395f1
commit d46df94f05
84 changed files with 1174 additions and 731 deletions
@@ -245,14 +245,14 @@ export function AccountBindingsTab({
return (
<>
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
<div className='grid grid-cols-1 gap-2.5 sm:grid-cols-2 sm:gap-3'>
{bindings.map((binding) => (
<div
key={binding.id}
className='flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between'
className='flex items-center justify-between gap-2.5 rounded-lg border p-2.5 sm:gap-3 sm:p-3'
>
<div className='flex min-w-0 items-center gap-3'>
<div className='bg-muted shrink-0 rounded-md p-2'>
<div className='flex min-w-0 items-center gap-2.5 sm:gap-3'>
<div className='bg-muted shrink-0 rounded-md p-1.5 sm:p-2'>
<binding.icon className='h-4 w-4' />
</div>
<div className='min-w-0'>
@@ -274,7 +274,7 @@ export function AccountBindingsTab({
<Button
variant='outline'
size='sm'
className='h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto'
className='h-7 shrink-0 px-2.5 text-xs'
onClick={binding.onBind}
disabled={binding.isBound && binding.id !== 'email'}
>
@@ -295,7 +295,7 @@ export function AccountBindingsTab({
<p className='text-muted-foreground mb-3 text-sm font-medium'>
{t('Custom OAuth')}
</p>
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
<div className='grid grid-cols-1 gap-2.5 sm:grid-cols-2 sm:gap-3'>
{customProviders.map((provider) => {
const binding = customBindings.find(
(b) => b.provider_id === provider.id
@@ -304,10 +304,10 @@ export function AccountBindingsTab({
return (
<div
key={provider.id}
className='flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between'
className='flex items-center justify-between gap-2.5 rounded-lg border p-2.5 sm:gap-3 sm:p-3'
>
<div className='flex min-w-0 items-center gap-3'>
<div className='bg-muted shrink-0 rounded-md p-2'>
<div className='flex min-w-0 items-center gap-2.5 sm:gap-3'>
<div className='bg-muted shrink-0 rounded-md p-1.5 sm:p-2'>
<Link2 className='h-4 w-4' />
</div>
<div className='min-w-0'>
@@ -332,7 +332,7 @@ export function AccountBindingsTab({
<Button
variant='ghost'
size='sm'
className='text-destructive hover:text-destructive h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto'
className='text-destructive hover:text-destructive h-7 shrink-0 px-2.5 text-xs'
onClick={() => setUnbindTarget(binding)}
>
<Unlink className='mr-1 h-3 w-3' />
@@ -342,7 +342,7 @@ export function AccountBindingsTab({
<Button
variant='outline'
size='sm'
className='h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto'
className='h-7 shrink-0 px-2.5 text-xs'
onClick={() => handleBindCustomOAuth(provider)}
>
{t('Bind')}
@@ -102,16 +102,16 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
}
return (
<div className='space-y-6'>
<div className='space-y-4 sm:space-y-6'>
{/* Notification Type */}
<div className='space-y-3'>
<div className='space-y-2.5'>
<Label>{t('Notification Method')}</Label>
<RadioGroup
value={settings.notify_type}
onValueChange={(value) =>
updateField('notify_type', value as NotifyType)
}
className='grid grid-cols-2 gap-3 sm:grid-cols-4'
className='grid grid-cols-4 gap-1.5 sm:gap-3'
>
{NOTIFICATION_METHODS.map((method) => {
const Icon = NOTIFICATION_ICONS[method.value]
@@ -120,7 +120,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
<Label
key={method.value}
htmlFor={method.value}
className={`flex cursor-pointer flex-col items-center gap-2 rounded-lg border-2 p-3 transition-colors ${
className={`flex min-h-16 cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border p-2 text-center transition-colors sm:min-h-20 sm:gap-2 sm:border-2 sm:p-3 ${
isSelected
? 'border-primary bg-primary/5 text-primary'
: 'border-muted hover:border-muted-foreground/25 hover:bg-muted/50'
@@ -131,8 +131,10 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
id={method.value}
className='sr-only'
/>
<Icon className='h-5 w-5' />
<span className='text-sm font-medium'>{t(method.label)}</span>
<Icon className='h-4 w-4 sm:h-5 sm:w-5' />
<span className='max-w-full truncate text-xs font-medium sm:text-sm'>
{t(method.label)}
</span>
</Label>
)
})}
@@ -140,11 +142,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</div>
{/* Warning Threshold */}
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='threshold'>{t('Quota Warning Threshold')}</Label>
<Input
id='threshold'
type='number'
className='h-9'
value={settings.quota_warning_threshold}
onChange={(e) =>
updateField('quota_warning_threshold', Number(e.target.value))
@@ -158,11 +161,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{/* Email Settings */}
{settings.notify_type === 'email' && (
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='notifyEmail'>{t('Notification Email')}</Label>
<Input
id='notifyEmail'
type='email'
className='h-9'
value={settings.notification_email}
onChange={(e) => updateField('notification_email', e.target.value)}
placeholder={t('Leave empty to use account email')}
@@ -173,17 +177,18 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{/* Webhook Settings */}
{settings.notify_type === 'webhook' && (
<>
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='webhookUrl'>{t('Webhook URL')}</Label>
<Input
id='webhookUrl'
type='url'
className='h-9'
value={settings.webhook_url}
onChange={(e) => updateField('webhook_url', e.target.value)}
placeholder={t('https://example.com/webhook')}
/>
</div>
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='webhookSecret'>{t('Webhook Secret')}</Label>
<PasswordInput
id='webhookSecret'
@@ -197,11 +202,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{/* Bark Settings */}
{settings.notify_type === 'bark' && (
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='barkUrl'>{t('Bark Push URL')}</Label>
<Input
id='barkUrl'
type='url'
className='h-9'
value={settings.bark_url}
onChange={(e) => updateField('bark_url', e.target.value)}
placeholder={t('https://api.day.app/yourkey/{{title}}/{{content}}')}
@@ -215,11 +221,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{/* Gotify Settings */}
{settings.notify_type === 'gotify' && (
<>
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='gotifyUrl'>{t('Gotify Server URL')}</Label>
<Input
id='gotifyUrl'
type='url'
className='h-9'
value={settings.gotify_url}
onChange={(e) => updateField('gotify_url', e.target.value)}
placeholder={t('https://gotify.example.com')}
@@ -228,7 +235,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{t('Enter the full URL of your Gotify server')}
</p>
</div>
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='gotifyToken'>{t('Gotify Application Token')}</Label>
<PasswordInput
id='gotifyToken'
@@ -240,11 +247,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{t('Token obtained from your Gotify application')}
</p>
</div>
<div className='space-y-2'>
<div className='space-y-1.5'>
<Label htmlFor='gotifyPriority'>{t('Message Priority')}</Label>
<Input
id='gotifyPriority'
type='number'
className='h-9'
min='0'
max='10'
value={settings.gotify_priority}
@@ -259,8 +267,8 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
)}
</p>
</div>
<div className='bg-muted/50 rounded-lg border p-4'>
<h5 className='mb-2 text-sm font-medium'>
<div className='bg-muted/50 rounded-lg border p-3 sm:p-4'>
<h5 className='mb-1.5 text-sm font-medium sm:mb-2'>
{t('Setup Instructions')}
</h5>
<ol className='text-muted-foreground space-y-1 text-xs'>
@@ -287,7 +295,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
<div className='border-t' />
{/* Preferences Section */}
<div className='space-y-4'>
<div className='space-y-3'>
<div>
<h4 className='text-sm font-medium'>{t('Preferences')}</h4>
<p className='text-muted-foreground mt-1 text-xs'>
@@ -297,12 +305,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{/* Receive Upstream Model Update Notifications (admin only) */}
{isAdmin && (
<div className='flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'>
<div className='flex items-start justify-between gap-3 rounded-lg border p-3 sm:items-center sm:p-4'>
<div className='space-y-0.5'>
<Label htmlFor='upstreamModelUpdateNotify'>
{t('Receive Upstream Model Update Notifications')}
</Label>
<p className='text-muted-foreground text-sm'>
<p className='text-muted-foreground line-clamp-3 text-xs sm:line-clamp-none sm:text-sm'>
{t(
'Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.'
)}
@@ -320,12 +328,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
)}
{/* Accept Unset Model Price */}
<div className='flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'>
<div className='flex items-start justify-between gap-3 rounded-lg border p-3 sm:items-center sm:p-4'>
<div className='space-y-0.5'>
<Label htmlFor='acceptUnsetPrice'>
{t('Accept Unpriced Models')}
</Label>
<p className='text-muted-foreground text-sm'>
<p className='text-muted-foreground text-xs sm:text-sm'>
{t('Allow using models without price configuration')}
</p>
</div>
@@ -340,10 +348,10 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</div>
{/* Record IP Log */}
<div className='flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'>
<div className='flex items-start justify-between gap-3 rounded-lg border p-3 sm:items-center sm:p-4'>
<div className='space-y-0.5'>
<Label htmlFor='recordIp'>{t('Record IP Address')}</Label>
<p className='text-muted-foreground text-sm'>
<p className='text-muted-foreground text-xs sm:text-sm'>
{t('Log IP address for usage and error logs')}
</p>
</div>