Преглед изворни кода

feat(ui): improve mobile responsive layouts

CaIon пре 1 недеља
родитељ
комит
d46df94f05
84 измењених фајлова са 1172 додато и 729 уклоњено
  1. 4 4
      web/default/src/components/data-table/mobile-card-list.tsx
  2. 5 5
      web/default/src/components/data-table/pagination.tsx
  3. 5 5
      web/default/src/components/data-table/toolbar.tsx
  4. 2 2
      web/default/src/components/data-table/view-options.tsx
  5. 9 7
      web/default/src/components/layout/components/section-page-layout.tsx
  6. 81 0
      web/default/src/components/ui/titled-card.tsx
  7. 19 3
      web/default/src/features/auth/hooks/use-auth-redirect.ts
  8. 5 2
      web/default/src/features/channels/components/channels-table.tsx
  9. 5 5
      web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  10. 4 4
      web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx
  11. 7 4
      web/default/src/features/dashboard/components/models/log-stat-cards.tsx
  12. 4 4
      web/default/src/features/dashboard/components/models/model-charts.tsx
  13. 9 9
      web/default/src/features/dashboard/components/models/models-filter-dialog.tsx
  14. 4 4
      web/default/src/features/dashboard/components/overview/announcements-panel.tsx
  15. 4 4
      web/default/src/features/dashboard/components/overview/api-info-item.tsx
  16. 3 3
      web/default/src/features/dashboard/components/overview/api-info-panel.tsx
  17. 2 2
      web/default/src/features/dashboard/components/overview/faq-panel.tsx
  18. 4 9
      web/default/src/features/dashboard/components/overview/summary-cards.tsx
  19. 5 5
      web/default/src/features/dashboard/components/overview/uptime-panel.tsx
  20. 6 6
      web/default/src/features/dashboard/components/ui/panel-wrapper.tsx
  21. 12 10
      web/default/src/features/dashboard/components/ui/stat-card.tsx
  22. 11 12
      web/default/src/features/dashboard/components/users/user-charts.tsx
  23. 2 6
      web/default/src/features/dashboard/hooks/use-dashboard-config.tsx
  24. 4 4
      web/default/src/features/dashboard/index.tsx
  25. 2 2
      web/default/src/features/dashboard/lib/filters.ts
  26. 13 5
      web/default/src/features/keys/components/api-key-group-combobox.tsx
  27. 32 25
      web/default/src/features/keys/components/api-keys-mutate-drawer.tsx
  28. 138 13
      web/default/src/features/keys/components/api-keys-table.tsx
  29. 2 2
      web/default/src/features/models/components/deployments-table.tsx
  30. 7 7
      web/default/src/features/models/components/dialogs/update-config-dialog.tsx
  31. 5 5
      web/default/src/features/models/components/dialogs/view-details-dialog.tsx
  32. 6 6
      web/default/src/features/models/components/dialogs/view-logs-dialog.tsx
  33. 4 4
      web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
  34. 5 5
      web/default/src/features/models/components/drawers/prefill-group-form-drawer.tsx
  35. 5 2
      web/default/src/features/models/components/models-table.tsx
  36. 64 4
      web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx
  37. 6 6
      web/default/src/features/pricing/components/filter-bar.tsx
  38. 3 3
      web/default/src/features/pricing/components/model-card.tsx
  39. 2 2
      web/default/src/features/pricing/components/model-details.tsx
  40. 3 3
      web/default/src/features/pricing/components/pricing-toolbar.tsx
  41. 5 5
      web/default/src/features/pricing/index.tsx
  42. 8 0
      web/default/src/features/profile/api.ts
  43. 136 0
      web/default/src/features/profile/components/language-preferences-card.tsx
  44. 86 74
      web/default/src/features/profile/components/passkey-card.tsx
  45. 16 16
      web/default/src/features/profile/components/profile-header.tsx
  46. 18 31
      web/default/src/features/profile/components/profile-security-card.tsx
  47. 14 29
      web/default/src/features/profile/components/profile-settings-card.tsx
  48. 6 6
      web/default/src/features/profile/components/sidebar-modules-card.tsx
  49. 11 11
      web/default/src/features/profile/components/tabs/account-bindings-tab.tsx
  50. 31 23
      web/default/src/features/profile/components/tabs/notification-tab.tsx
  51. 8 8
      web/default/src/features/profile/components/two-fa-card.tsx
  52. 10 5
      web/default/src/features/profile/index.tsx
  53. 2 0
      web/default/src/features/profile/types.ts
  54. 5 5
      web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
  55. 2 2
      web/default/src/features/redemption-codes/components/redemptions-table.tsx
  56. 5 5
      web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx
  57. 9 9
      web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx
  58. 1 1
      web/default/src/features/subscriptions/components/subscriptions-table.tsx
  59. 1 1
      web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
  60. 7 7
      web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx
  61. 22 19
      web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  62. 6 6
      web/default/src/features/usage-logs/components/dialogs/usage-logs-filter-dialog.tsx
  63. 3 3
      web/default/src/features/usage-logs/components/task-logs-filter-bar.tsx
  64. 4 4
      web/default/src/features/usage-logs/components/usage-logs-table.tsx
  65. 4 4
      web/default/src/features/users/components/users-mutate-drawer.tsx
  66. 2 2
      web/default/src/features/users/components/users-table.tsx
  67. 57 108
      web/default/src/features/wallet/components/affiliate-rewards-card.tsx
  68. 3 3
      web/default/src/features/wallet/components/creem-products-section.tsx
  69. 13 13
      web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx
  70. 3 3
      web/default/src/features/wallet/components/dialogs/creem-confirm-dialog.tsx
  71. 3 3
      web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx
  72. 3 3
      web/default/src/features/wallet/components/dialogs/transfer-dialog.tsx
  73. 40 53
      web/default/src/features/wallet/components/recharge-form-card.tsx
  74. 31 35
      web/default/src/features/wallet/components/subscription-plans-card.tsx
  75. 5 5
      web/default/src/features/wallet/components/wallet-stats-card.tsx
  76. 28 13
      web/default/src/features/wallet/index.tsx
  77. 6 0
      web/default/src/i18n/locales/en.json
  78. 6 0
      web/default/src/i18n/locales/fr.json
  79. 6 0
      web/default/src/i18n/locales/ja.json
  80. 6 0
      web/default/src/i18n/locales/ru.json
  81. 6 0
      web/default/src/i18n/locales/vi.json
  82. 6 0
      web/default/src/i18n/locales/zh.json
  83. 14 0
      web/default/src/lib/time.ts
  84. 1 1
      web/default/src/stores/auth-store.ts

+ 4 - 4
web/default/src/components/data-table/mobile-card-list.tsx

@@ -62,7 +62,7 @@ function ListSkeleton() {
             <Skeleton className='h-4 w-32' />
             <Skeleton className='h-4 w-32' />
             <Skeleton className='h-5 w-16 rounded-full' />
             <Skeleton className='h-5 w-16 rounded-full' />
           </div>
           </div>
-          <div className='mt-1.5 flex items-start gap-4'>
+          <div className='mt-1.5 grid grid-cols-2 gap-2'>
             <div className='flex-1'>
             <div className='flex-1'>
               <Skeleton className='mb-1 h-2 w-8' />
               <Skeleton className='mb-1 h-2 w-8' />
               <Skeleton className='h-4 w-full' />
               <Skeleton className='h-4 w-full' />
@@ -136,9 +136,9 @@ function CompactRow<TData>({ row }: { row: Row<TData> }) {
         )}
         )}
       </div>
       </div>
 
 
-      {/* Row 2: Key fields side by side */}
+      {/* Row 2: Key fields wrap into compact columns instead of squeezing */}
       {fieldCells.length > 0 && (
       {fieldCells.length > 0 && (
-        <div className='mt-1.5 flex items-start gap-4'>
+        <div className='mt-1.5 grid grid-cols-2 gap-x-3 gap-y-1.5'>
           {fieldCells.map((cell) => {
           {fieldCells.map((cell) => {
             const label = getCellLabel(cell)
             const label = getCellLabel(cell)
             return (
             return (
@@ -260,7 +260,7 @@ export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
 
 
   if (!rows || rows.length === 0) {
   if (!rows || rows.length === 0) {
     return (
     return (
-      <div className='rounded-lg border p-8'>
+      <div className='rounded-lg border p-6'>
         <Empty className='border-none p-0'>
         <Empty className='border-none p-0'>
           <EmptyHeader>
           <EmptyHeader>
             <EmptyMedia variant='icon'>
             <EmptyMedia variant='icon'>

+ 5 - 5
web/default/src/components/data-table/pagination.tsx

@@ -32,12 +32,12 @@ export function DataTablePagination<TData>({
     <div
     <div
       className={cn(
       className={cn(
         'flex items-center justify-between overflow-clip',
         'flex items-center justify-between overflow-clip',
-        '@max-2xl/content:flex-col-reverse @max-2xl/content:gap-4'
+        '@max-2xl/content:flex-col-reverse @max-2xl/content:gap-2 sm:@max-2xl/content:gap-4'
       )}
       )}
       style={{ overflowClipMargin: 1 }}
       style={{ overflowClipMargin: 1 }}
     >
     >
-      <div className='flex w-full items-center justify-between'>
-        <div className='flex min-w-[130px] items-center text-sm font-medium whitespace-nowrap @2xl/content:hidden'>
+      <div className='flex w-full items-center justify-between gap-2'>
+        <div className='flex min-w-0 items-center text-xs font-medium whitespace-nowrap sm:min-w-[130px] sm:text-sm @2xl/content:hidden'>
           {t('Page {{current}} of {{total}}', {
           {t('Page {{current}} of {{total}}', {
             current: currentPage,
             current: currentPage,
             total: totalPages,
             total: totalPages,
@@ -50,7 +50,7 @@ export function DataTablePagination<TData>({
               table.setPageSize(Number(value))
               table.setPageSize(Number(value))
             }}
             }}
           >
           >
-            <SelectTrigger className='h-8 w-[70px]'>
+            <SelectTrigger className='h-8 w-[64px] sm:w-[70px]'>
               <SelectValue placeholder={table.getState().pagination.pageSize} />
               <SelectValue placeholder={table.getState().pagination.pageSize} />
             </SelectTrigger>
             </SelectTrigger>
             <SelectContent side='top'>
             <SelectContent side='top'>
@@ -74,7 +74,7 @@ export function DataTablePagination<TData>({
             total: totalPages,
             total: totalPages,
           })}
           })}
         </div>
         </div>
-        <div className='flex items-center space-x-2'>
+        <div className='flex items-center space-x-1.5 sm:space-x-2'>
           <Button
           <Button
             variant='outline'
             variant='outline'
             className='size-8 p-0 @max-md/content:hidden'
             className='size-8 p-0 @max-md/content:hidden'

+ 5 - 5
web/default/src/components/data-table/toolbar.tsx

@@ -64,14 +64,14 @@ export function DataTableToolbar<TData>({
       onChange={(event) =>
       onChange={(event) =>
         table.getColumn(searchKey)?.setFilterValue(event.target.value)
         table.getColumn(searchKey)?.setFilterValue(event.target.value)
       }
       }
-      className='h-8 w-full sm:w-[150px] lg:w-[250px]'
+      className='h-9 w-full sm:h-8 sm:w-[150px] lg:w-[250px]'
     />
     />
   ) : (
   ) : (
     <Input
     <Input
       placeholder={resolvedSearchPlaceholder}
       placeholder={resolvedSearchPlaceholder}
       value={table.getState().globalFilter ?? ''}
       value={table.getState().globalFilter ?? ''}
       onChange={(event) => table.setGlobalFilter(event.target.value)}
       onChange={(event) => table.setGlobalFilter(event.target.value)}
-      className='h-8 w-full sm:w-[150px] lg:w-[250px]'
+      className='h-9 w-full sm:h-8 sm:w-[150px] lg:w-[250px]'
     />
     />
   )
   )
 
 
@@ -106,7 +106,7 @@ export function DataTableToolbar<TData>({
 
 
   return (
   return (
     <div className='space-y-2'>
     <div className='space-y-2'>
-      <div className='flex items-center gap-2'>
+      <div className='flex items-center gap-1.5 sm:gap-2'>
         {/* Search input */}
         {/* Search input */}
         {customSearch !== undefined ? customSearch : searchInput}
         {customSearch !== undefined ? customSearch : searchInput}
 
 
@@ -122,7 +122,7 @@ export function DataTableToolbar<TData>({
           <Button
           <Button
             variant='outline'
             variant='outline'
             size='sm'
             size='sm'
-            className='relative h-8 shrink-0 gap-1 sm:hidden'
+            className='relative h-9 shrink-0 gap-1 px-2 sm:hidden'
             onClick={() => setMobileFiltersOpen((v) => !v)}
             onClick={() => setMobileFiltersOpen((v) => !v)}
           >
           >
             <SlidersHorizontal className='h-3.5 w-3.5' />
             <SlidersHorizontal className='h-3.5 w-3.5' />
@@ -142,7 +142,7 @@ export function DataTableToolbar<TData>({
 
 
       {/* Mobile: collapsible filter area */}
       {/* Mobile: collapsible filter area */}
       {hasFilterContent && mobileFiltersOpen && (
       {hasFilterContent && mobileFiltersOpen && (
-        <div className='flex flex-wrap items-center gap-2 sm:hidden'>
+        <div className='bg-muted/30 flex flex-wrap items-center gap-2 rounded-lg border p-2 sm:hidden'>
           {additionalSearch && <div className='w-full'>{additionalSearch}</div>}
           {additionalSearch && <div className='w-full'>{additionalSearch}</div>}
           {filterChips}
           {filterChips}
           {resetButton}
           {resetButton}

+ 2 - 2
web/default/src/components/data-table/view-options.tsx

@@ -25,10 +25,10 @@ export function DataTableViewOptions<TData>({
         <Button
         <Button
           variant='outline'
           variant='outline'
           size='sm'
           size='sm'
-          className='ms-auto hidden h-8 lg:flex'
+          className='ms-auto h-9 w-9 px-0 sm:h-8 sm:w-auto sm:px-3 lg:flex'
         >
         >
           <MixerHorizontalIcon className='size-4' />
           <MixerHorizontalIcon className='size-4' />
-          {t('View')}
+          <span className='hidden sm:inline'>{t('View')}</span>
         </Button>
         </Button>
       </DropdownMenuTrigger>
       </DropdownMenuTrigger>
       <DropdownMenuContent align='end' className='w-[150px]'>
       <DropdownMenuContent align='end' className='w-[150px]'>

+ 9 - 7
web/default/src/components/layout/components/section-page-layout.tsx

@@ -70,15 +70,15 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
       <AppHeader />
       <AppHeader />
 
 
       <Main>
       <Main>
-        <div className='shrink-0 px-4 pt-4 pb-3 sm:pt-6 sm:pb-4'>
-          {breadcrumb != null && <div className='mb-3'>{breadcrumb}</div>}
-          <div className='flex flex-wrap items-center justify-between gap-x-4 gap-y-2'>
+        <div className='shrink-0 px-3 pt-3 pb-2.5 sm:px-4 sm:pt-6 sm:pb-4'>
+          {breadcrumb != null && <div className='mb-2 sm:mb-3'>{breadcrumb}</div>}
+          <div className='flex flex-wrap items-center justify-between gap-x-3 gap-y-2 sm:gap-x-4'>
             <div className='min-w-0'>
             <div className='min-w-0'>
-              <h2 className='text-base font-bold tracking-tight sm:text-lg'>
+              <h2 className='truncate text-base font-bold tracking-tight sm:text-lg'>
                 {title}
                 {title}
               </h2>
               </h2>
               {description != null && (
               {description != null && (
-                <p className='text-muted-foreground max-sm:text-xs sm:text-sm'>
+                <p className='text-muted-foreground line-clamp-2 max-sm:text-xs sm:text-sm'>
                   {description}
                   {description}
                 </p>
                 </p>
               )}
               )}
@@ -91,11 +91,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className='min-h-0 flex-1 overflow-auto px-4 pb-4'>{content}</div>
+        <div className='min-h-0 flex-1 overflow-auto px-3 pb-3 sm:px-4 sm:pb-4'>
+          {content}
+        </div>
 
 
         <div
         <div
           ref={setFooterContainer}
           ref={setFooterContainer}
-          className='bg-background shrink-0 border-t px-4 py-3 empty:hidden'
+          className='bg-background shrink-0 border-t px-3 py-2.5 empty:hidden sm:px-4 sm:py-3'
         />
         />
       </Main>
       </Main>
     </PageFooterProvider>
     </PageFooterProvider>

+ 81 - 0
web/default/src/components/ui/titled-card.tsx

@@ -0,0 +1,81 @@
+import type { ReactNode } from 'react'
+import { cn } from '@/lib/utils'
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from './card'
+
+type TitledCardProps = {
+  title: ReactNode
+  description?: ReactNode
+  icon?: ReactNode
+  action?: ReactNode
+  children?: ReactNode
+  className?: string
+  headerClassName?: string
+  contentClassName?: string
+  iconClassName?: string
+  titleClassName?: string
+  descriptionClassName?: string
+}
+
+export function TitledCard({
+  title,
+  description,
+  icon,
+  action,
+  children,
+  className,
+  headerClassName,
+  contentClassName,
+  iconClassName,
+  titleClassName,
+  descriptionClassName,
+}: TitledCardProps) {
+  return (
+    <Card className={cn('gap-0 overflow-hidden py-0', className)}>
+      <CardHeader
+        className={cn('border-b p-3 !pb-3 sm:p-5 sm:!pb-5', headerClassName)}
+      >
+        <div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
+          <div className='flex min-w-0 items-center gap-3'>
+            {icon != null && (
+              <div
+                className={cn(
+                  'bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg sm:h-9 sm:w-9',
+                  iconClassName
+                )}
+              >
+                {icon}
+              </div>
+            )}
+            <div className='min-w-0'>
+              <CardTitle
+                className={cn('text-lg tracking-tight sm:text-xl', titleClassName)}
+              >
+                {title}
+              </CardTitle>
+              {description != null && (
+                <CardDescription
+                  className={cn(
+                    'text-xs sm:text-sm',
+                    descriptionClassName
+                  )}
+                >
+                  {description}
+                </CardDescription>
+              )}
+            </div>
+          </div>
+          {action != null && <div className='w-full shrink-0 sm:w-auto'>{action}</div>}
+        </div>
+      </CardHeader>
+      <CardContent className={cn('p-3 sm:p-5', contentClassName)}>
+        {children}
+      </CardContent>
+    </Card>
+  )
+}

+ 19 - 3
web/default/src/features/auth/hooks/use-auth-redirect.ts

@@ -5,6 +5,24 @@ import { getSelf } from '@/lib/api'
 import type { User } from '@/features/users/types'
 import type { User } from '@/features/users/types'
 import { saveUserId } from '../lib/storage'
 import { saveUserId } from '../lib/storage'
 
 
+function getSavedLanguage(user: User): string | undefined {
+  const userData = user as Record<string, unknown>
+  if (typeof userData.language === 'string') {
+    return userData.language
+  }
+
+  if (typeof userData.setting !== 'string') {
+    return undefined
+  }
+
+  try {
+    const setting = JSON.parse(userData.setting) as { language?: unknown }
+    return typeof setting.language === 'string' ? setting.language : undefined
+  } catch {
+    return undefined
+  }
+}
+
 /**
 /**
  * Hook for handling authentication redirects and user data management
  * Hook for handling authentication redirects and user data management
  */
  */
@@ -39,9 +57,7 @@ export function useAuthRedirect() {
         }
         }
 
 
         // Restore saved language preference
         // Restore saved language preference
-        const savedLang = (user as Record<string, unknown>).language as
-          | string
-          | undefined
+        const savedLang = getSavedLanguage(user)
         if (savedLang && savedLang !== i18n.language) {
         if (savedLang && savedLang !== i18n.language) {
           i18n.changeLanguage(savedLang)
           i18n.changeLanguage(savedLang)
         }
         }

+ 5 - 2
web/default/src/features/channels/components/channels-table.tsx

@@ -87,7 +87,10 @@ export function ChannelsTable() {
   } = useTableUrlState({
   } = useTableUrlState({
     search: route.useSearch(),
     search: route.useSearch(),
     navigate: route.useNavigate(),
     navigate: route.useNavigate(),
-    pagination: { defaultPage: 1, defaultPageSize: DEFAULT_PAGE_SIZE },
+    pagination: {
+      defaultPage: 1,
+      defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE,
+    },
     globalFilter: { enabled: true, key: 'filter' },
     globalFilter: { enabled: true, key: 'filter' },
     columnFilters: [
     columnFilters: [
       { columnId: 'status', searchKey: 'status', type: 'array' },
       { columnId: 'status', searchKey: 'status', type: 'array' },
@@ -329,7 +332,7 @@ export function ChannelsTable() {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         <DataTableToolbar
         <DataTableToolbar
           table={table}
           table={table}
           searchPlaceholder={t('Filter by name, ID, or key...')}
           searchPlaceholder={t('Filter by name, ID, or key...')}

+ 5 - 5
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx

@@ -1082,8 +1082,8 @@ export function ChannelMutateDrawer({
   return (
   return (
     <>
     <>
       <Sheet open={open} onOpenChange={handleOpenChange}>
       <Sheet open={open} onOpenChange={handleOpenChange}>
-        <SheetContent className='flex w-full flex-col sm:max-w-3xl'>
-          <SheetHeader className='text-start'>
+        <SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-3xl'>
+          <SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
             <SheetTitle className='flex items-center gap-3'>
             <SheetTitle className='flex items-center gap-3'>
               <span className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border'>
               <span className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border'>
                 {getLobeIcon(`${getChannelTypeIcon(currentType)}.Color`, 22)}
                 {getLobeIcon(`${getChannelTypeIcon(currentType)}.Color`, 22)}
@@ -1110,10 +1110,10 @@ export function ChannelMutateDrawer({
             <form
             <form
               id='channel-form'
               id='channel-form'
               onSubmit={form.handleSubmit(onSubmit)}
               onSubmit={form.handleSubmit(onSubmit)}
-              className='flex-1 space-y-5 overflow-y-auto px-4 pb-2'
+              className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-5 sm:px-4'
             >
             >
               {/* ── Basic Information ── */}
               {/* ── Basic Information ── */}
-              <div className='bg-card space-y-4 rounded-xl border p-5'>
+              <div className='bg-card space-y-4 rounded-xl border p-3 sm:p-5'>
                 <CardHeading
                 <CardHeading
                   title={t('Basic Information')}
                   title={t('Basic Information')}
                   icon={<Server className='h-4 w-4' />}
                   icon={<Server className='h-4 w-4' />}
@@ -3276,7 +3276,7 @@ export function ChannelMutateDrawer({
             </form>
             </form>
           </Form>
           </Form>
 
 
-          <SheetFooter className='gap-2'>
+          <SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
             <SheetClose asChild>
             <SheetClose asChild>
               <Button variant='outline' disabled={isSubmitting}>
               <Button variant='outline' disabled={isSubmitting}>
                 {t('Cancel')}
                 {t('Cancel')}

+ 4 - 4
web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx

@@ -77,7 +77,7 @@ export function ConsumptionDistributionChart(
 
 
   return (
   return (
     <div className='overflow-hidden rounded-lg border'>
     <div className='overflow-hidden rounded-lg border'>
-      <div className='flex w-full flex-col gap-3 border-b px-4 py-3 sm:px-5 lg:flex-row lg:items-center lg:justify-between'>
+      <div className='flex w-full flex-col gap-1.5 border-b px-3 py-2 sm:gap-3 sm:px-5 sm:py-3 lg:flex-row lg:items-center lg:justify-between'>
         <div className='flex items-center gap-2'>
         <div className='flex items-center gap-2'>
           <WalletCards className='text-muted-foreground/60 size-4' />
           <WalletCards className='text-muted-foreground/60 size-4' />
           <div className='text-sm font-semibold'>
           <div className='text-sm font-semibold'>
@@ -88,7 +88,7 @@ export function ConsumptionDistributionChart(
           </span>
           </span>
         </div>
         </div>
 
 
-        <div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
+        <div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-md border p-0.5 sm:h-8 sm:w-auto'>
           {CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
           {CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
             const Icon = CHART_TYPE_ICONS[item.value]
             const Icon = CHART_TYPE_ICONS[item.value]
             return (
             return (
@@ -96,7 +96,7 @@ export function ConsumptionDistributionChart(
                 key={item.value}
                 key={item.value}
                 type='button'
                 type='button'
                 onClick={() => setChartType(item.value)}
                 onClick={() => setChartType(item.value)}
-                className={`inline-flex items-center gap-1.5 rounded-[5px] px-3 text-xs font-medium transition-colors ${
+                className={`inline-flex shrink-0 items-center gap-1.5 rounded-[5px] px-3 text-xs font-medium transition-colors ${
                   chartType === item.value
                   chartType === item.value
                     ? 'bg-background text-foreground shadow-sm'
                     ? 'bg-background text-foreground shadow-sm'
                     : 'text-muted-foreground hover:text-foreground'
                     : 'text-muted-foreground hover:text-foreground'
@@ -110,7 +110,7 @@ export function ConsumptionDistributionChart(
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className='h-96 p-2'>
+      <div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
         {themeReady && spec && (
         {themeReady && spec && (
           <VChart
           <VChart
             key={`${chartType}-${resolvedTheme}`}
             key={`${chartType}-${resolvedTheme}`}

+ 7 - 4
web/default/src/features/dashboard/components/models/log-stat-cards.tsx

@@ -95,10 +95,13 @@ export function LogStatCards(props: LogStatCardsProps) {
   return (
   return (
     <div className='overflow-hidden rounded-lg border'>
     <div className='overflow-hidden rounded-lg border'>
       <div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-3 lg:grid-cols-5'>
       <div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-3 lg:grid-cols-5'>
-        {items.map((it) => {
+        {items.map((it, idx) => {
           const Icon = it.icon
           const Icon = it.icon
           return (
           return (
-            <div key={it.title} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+            <div
+              key={it.title}
+              className={`px-3 py-2.5 sm:px-5 sm:py-4 ${idx === items.length - 1 && items.length % 2 !== 0 ? 'col-span-2 sm:col-span-1' : ''}`}
+            >
               <div className='flex items-center gap-2'>
               <div className='flex items-center gap-2'>
                 <Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
                 <Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
                 <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
                 <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
@@ -113,7 +116,7 @@ export function LogStatCards(props: LogStatCardsProps) {
                 </div>
                 </div>
               ) : error ? (
               ) : error ? (
                 <>
                 <>
-                  <div className='text-muted-foreground mt-2 font-mono text-2xl font-bold tracking-tight tabular-nums'>
+                  <div className='text-muted-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
                     --
                     --
                   </div>
                   </div>
                   <div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
                   <div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
@@ -122,7 +125,7 @@ export function LogStatCards(props: LogStatCardsProps) {
                 </>
                 </>
               ) : (
               ) : (
                 <>
                 <>
-                  <div className='text-foreground mt-2 font-mono text-2xl font-bold tracking-tight tabular-nums'>
+                  <div className='text-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
                     {it.value}
                     {it.value}
                   </div>
                   </div>
                   <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
                   <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>

+ 4 - 4
web/default/src/features/dashboard/components/models/model-charts.tsx

@@ -78,7 +78,7 @@ export function ModelCharts(props: ModelChartsProps) {
 
 
   return (
   return (
     <div className='overflow-hidden rounded-lg border'>
     <div className='overflow-hidden rounded-lg border'>
-      <div className='flex w-full flex-col gap-3 border-b px-4 py-3 sm:px-5 lg:flex-row lg:items-center lg:justify-between'>
+      <div className='flex w-full flex-col gap-1.5 border-b px-3 py-2 sm:gap-3 sm:px-5 sm:py-3 lg:flex-row lg:items-center lg:justify-between'>
         <div className='flex items-center gap-2'>
         <div className='flex items-center gap-2'>
           <PieChartIcon className='text-muted-foreground/60 size-4' />
           <PieChartIcon className='text-muted-foreground/60 size-4' />
           <div className='text-sm font-semibold'>
           <div className='text-sm font-semibold'>
@@ -89,13 +89,13 @@ export function ModelCharts(props: ModelChartsProps) {
           </span>
           </span>
         </div>
         </div>
 
 
-        <div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
+        <div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-md border p-0.5 sm:h-8 sm:w-auto'>
           {MODEL_ANALYTICS_CHART_OPTIONS.map((tab) => (
           {MODEL_ANALYTICS_CHART_OPTIONS.map((tab) => (
             <button
             <button
               key={tab.value}
               key={tab.value}
               type='button'
               type='button'
               onClick={() => setActiveTab(tab.value)}
               onClick={() => setActiveTab(tab.value)}
-              className={`rounded-[5px] px-3 text-xs font-medium transition-colors ${
+              className={`shrink-0 rounded-[5px] px-3 text-xs font-medium transition-colors ${
                 activeTab === tab.value
                 activeTab === tab.value
                   ? 'bg-background text-foreground shadow-sm'
                   ? 'bg-background text-foreground shadow-sm'
                   : 'text-muted-foreground hover:text-foreground'
                   : 'text-muted-foreground hover:text-foreground'
@@ -107,7 +107,7 @@ export function ModelCharts(props: ModelChartsProps) {
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className='h-96 p-2'>
+      <div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
         {themeReady && spec && (
         {themeReady && spec && (
           <VChart
           <VChart
             key={`${activeTab}-${resolvedTheme}`}
             key={`${activeTab}-${resolvedTheme}`}

+ 9 - 9
web/default/src/features/dashboard/components/models/models-filter-dialog.tsx

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
 import { Filter, RotateCcw, Calendar, Search } from 'lucide-react'
 import { Filter, RotateCcw, Calendar, Search } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { useAuthStore } from '@/stores/auth-store'
 import { useAuthStore } from '@/stores/auth-store'
-import { getNormalizedDateRange, type TimeGranularity } from '@/lib/time'
+import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import {
 import {
@@ -88,7 +88,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
 
 
   const handleReset = () => {
   const handleReset = () => {
     const days = props.preferences.defaultTimeRangeDays
     const days = props.preferences.defaultTimeRangeDays
-    const { start, end } = getNormalizedDateRange(days)
+    const { start, end } = getRollingDateRange(days)
     setFilters({
     setFilters({
       ...buildDefaultDashboardFilters(props.preferences),
       ...buildDefaultDashboardFilters(props.preferences),
       start_timestamp: start,
       start_timestamp: start,
@@ -109,7 +109,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
   }
   }
 
 
   const handleQuickRange = (days: number) => {
   const handleQuickRange = (days: number) => {
-    const { start, end } = getNormalizedDateRange(days)
+    const { start, end } = getRollingDateRange(days)
 
 
     setFilters((prev) => ({
     setFilters((prev) => ({
       ...prev,
       ...prev,
@@ -127,7 +127,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
           {t('Filter')}
           {t('Filter')}
         </Button>
         </Button>
       </DialogTrigger>
       </DialogTrigger>
-      <DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col sm:max-w-lg'>
+      <DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-lg'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t('Filter Dashboard Models')}</DialogTitle>
           <DialogTitle>{t('Filter Dashboard Models')}</DialogTitle>
           <DialogDescription>
           <DialogDescription>
@@ -137,15 +137,15 @@ export function ModelsFilter(props: ModelsFilterProps) {
           </DialogDescription>
           </DialogDescription>
         </DialogHeader>
         </DialogHeader>
 
 
-        <ScrollArea className='flex-1 pr-4'>
-          <div className='grid gap-4 py-4'>
+        <ScrollArea className='flex-1 pr-3 sm:pr-4'>
+          <div className='grid gap-3 py-3 sm:gap-4 sm:py-4'>
             {/* Quick time range selection */}
             {/* Quick time range selection */}
             <div className='grid gap-2'>
             <div className='grid gap-2'>
               <Label className='flex items-center gap-2'>
               <Label className='flex items-center gap-2'>
                 <Calendar className='h-4 w-4' />
                 <Calendar className='h-4 w-4' />
                 {t('Quick Range')}
                 {t('Quick Range')}
               </Label>
               </Label>
-              <div className='flex gap-2'>
+              <div className='grid grid-cols-2 gap-2 sm:flex'>
                 {TIME_RANGE_PRESETS.map((range) => (
                 {TIME_RANGE_PRESETS.map((range) => (
                   <Button
                   <Button
                     key={range.days}
                     key={range.days}
@@ -170,7 +170,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
             <SectionDivider label={t('Custom Time Range')} />
             <SectionDivider label={t('Custom Time Range')} />
 
 
             {/* Custom time range */}
             {/* Custom time range */}
-            <div className='grid gap-4'>
+            <div className='grid gap-3 sm:gap-4'>
               <div className='grid gap-2'>
               <div className='grid gap-2'>
                 <Label htmlFor='start_timestamp'>{t('Start Time')}</Label>
                 <Label htmlFor='start_timestamp'>{t('Start Time')}</Label>
                 <DateTimePicker
                 <DateTimePicker
@@ -236,7 +236,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
           </div>
           </div>
         </ScrollArea>
         </ScrollArea>
 
 
-        <DialogFooter>
+        <DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
           <Button onClick={handleReset} variant='outline' type='button'>
           <Button onClick={handleReset} variant='outline' type='button'>
             <RotateCcw className='mr-2 h-4 w-4' />
             <RotateCcw className='mr-2 h-4 w-4' />
             {t('Reset')}
             {t('Reset')}

+ 4 - 4
web/default/src/features/dashboard/components/overview/announcements-panel.tsx

@@ -47,10 +47,10 @@ export function AnnouncementsPanel() {
       loading={loading}
       loading={loading}
       empty={!list.length}
       empty={!list.length}
       emptyMessage={t('No announcements at this time')}
       emptyMessage={t('No announcements at this time')}
-      height='h-64'
+      height='h-56 sm:h-64'
     >
     >
-      <ScrollArea className='h-64'>
-        <div className='-mx-4 sm:-mx-5'>
+      <ScrollArea className='h-56 sm:h-64'>
+        <div className='-mx-3 sm:-mx-5'>
           {list.map((item: AnnouncementItem, idx: number) => {
           {list.map((item: AnnouncementItem, idx: number) => {
             const key = item.id ?? `announcement-${idx}`
             const key = item.id ?? `announcement-${idx}`
             return (
             return (
@@ -59,7 +59,7 @@ export function AnnouncementsPanel() {
                 type='button'
                 type='button'
                 onClick={() => handleAnnouncementClick(item)}
                 onClick={() => handleAnnouncementClick(item)}
                 className={cn(
                 className={cn(
-                  'group hover:bg-muted/40 w-full px-4 py-3.5 text-left transition-colors sm:px-5',
+                  'group hover:bg-muted/40 w-full px-3 py-3 text-left transition-colors sm:px-5 sm:py-3.5',
                   idx < list.length - 1 && 'border-border/60 border-b'
                   idx < list.length - 1 && 'border-border/60 border-b'
                 )}
                 )}
               >
               >

+ 4 - 4
web/default/src/features/dashboard/components/overview/api-info-item.tsx

@@ -23,8 +23,8 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
   const status = props.status
   const status = props.status
 
 
   return (
   return (
-    <div className='group hover:bg-muted/40 flex items-center justify-between gap-3 px-4 py-3 transition-colors sm:px-5'>
-      <div className='flex min-w-0 flex-1 items-center gap-3'>
+    <div className='group hover:bg-muted/40 flex items-center justify-between gap-2 px-3 py-2.5 transition-colors sm:gap-3 sm:px-5 sm:py-3'>
+      <div className='flex min-w-0 flex-1 items-center gap-2 sm:gap-3'>
         <span
         <span
           className={cn(
           className={cn(
             'inline-block size-2 shrink-0 rounded-full',
             'inline-block size-2 shrink-0 rounded-full',
@@ -91,7 +91,7 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
             variant='ghost'
             variant='ghost'
             size='sm'
             size='sm'
             onClick={() => openExternalSpeedTest(item.url)}
             onClick={() => openExternalSpeedTest(item.url)}
-            className='size-7 p-0'
+            className='hidden size-7 p-0 sm:inline-flex'
             title={t('External Speed Test')}
             title={t('External Speed Test')}
           >
           >
             <Gauge className='size-3.5' />
             <Gauge className='size-3.5' />
@@ -111,7 +111,7 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
             variant='ghost'
             variant='ghost'
             size='sm'
             size='sm'
             asChild
             asChild
-            className='size-7 p-0'
+            className='hidden size-7 p-0 sm:inline-flex'
             title={t('Open in New Tab')}
             title={t('Open in New Tab')}
           >
           >
             <a href={item.url} target='_blank' rel='noreferrer'>
             <a href={item.url} target='_blank' rel='noreferrer'>

+ 3 - 3
web/default/src/features/dashboard/components/overview/api-info-panel.tsx

@@ -37,10 +37,10 @@ export function ApiInfoPanel() {
       loading={loading}
       loading={loading}
       empty={!list.length}
       empty={!list.length}
       emptyMessage={t('No API routes configured')}
       emptyMessage={t('No API routes configured')}
-      height='h-64'
+      height='h-56 sm:h-64'
     >
     >
-      <ScrollArea className='h-64'>
-        <div className='-mx-4 sm:-mx-5'>
+      <ScrollArea className='h-56 sm:h-64'>
+        <div className='-mx-3 sm:-mx-5'>
           {list.map((item: ApiInfoItem, idx: number) => (
           {list.map((item: ApiInfoItem, idx: number) => (
             <div
             <div
               key={item.url}
               key={item.url}

+ 2 - 2
web/default/src/features/dashboard/components/overview/faq-panel.tsx

@@ -27,9 +27,9 @@ export function FAQPanel() {
       loading={loading}
       loading={loading}
       empty={!list.length}
       empty={!list.length}
       emptyMessage={t('No FAQ entries available')}
       emptyMessage={t('No FAQ entries available')}
-      height='h-80'
+      height='h-64 sm:h-80'
     >
     >
-      <ScrollArea className='h-80'>
+      <ScrollArea className='h-64 sm:h-80'>
         <Accordion type='single' collapsible className='w-full'>
         <Accordion type='single' collapsible className='w-full'>
           {list.map((item: FAQItem, idx: number) => {
           {list.map((item: FAQItem, idx: number) => {
             const key = item.id ?? `faq-${idx}`
             const key = item.id ?? `faq-${idx}`

+ 4 - 9
web/default/src/features/dashboard/components/overview/summary-cards.tsx

@@ -53,14 +53,9 @@ export function SummaryCards() {
 
 
   return (
   return (
     <div className='overflow-hidden rounded-lg border'>
     <div className='overflow-hidden rounded-lg border'>
-      <StaggerContainer className='grid sm:grid-cols-2 lg:grid-cols-3'>
-        {items.map((it, idx) => (
-          <StaggerItem
-            key={it.title}
-            className={`px-4 sm:px-5 ${
-              idx > 0 ? 'border-t sm:border-t-0 sm:border-l' : ''
-            }`}
-          >
+      <StaggerContainer className='divide-border/60 grid grid-cols-3 divide-x'>
+        {items.map((it) => (
+          <StaggerItem key={it.title} className='px-3 py-3 sm:px-5 sm:py-4'>
             <StatCard
             <StatCard
               title={it.title}
               title={it.title}
               value={it.value}
               value={it.value}
@@ -72,7 +67,7 @@ export function SummaryCards() {
                   <Button
                   <Button
                     variant='outline'
                     variant='outline'
                     size='sm'
                     size='sm'
-                    className='h-6 gap-1 px-2 text-xs'
+                    className='hidden h-6 gap-1 px-2 text-xs sm:inline-flex'
                     asChild
                     asChild
                   >
                   >
                     <Link to='/wallet'>
                     <Link to='/wallet'>

+ 5 - 5
web/default/src/features/dashboard/components/overview/uptime-panel.tsx

@@ -84,7 +84,7 @@ export function UptimePanel() {
       loading={loading}
       loading={loading}
       empty={!groups.length}
       empty={!groups.length}
       emptyMessage={t('No uptime monitoring configured')}
       emptyMessage={t('No uptime monitoring configured')}
-      height='h-80'
+      height='h-64 sm:h-80'
       headerActions={
       headerActions={
         <Button
         <Button
           variant='ghost'
           variant='ghost'
@@ -100,11 +100,11 @@ export function UptimePanel() {
         </Button>
         </Button>
       }
       }
     >
     >
-      <ScrollArea className='h-80'>
-        <div className='-mx-4 space-y-0 sm:-mx-5'>
+      <ScrollArea className='h-64 sm:h-80'>
+        <div className='-mx-3 space-y-0 sm:-mx-5'>
           {groups.map((group, groupIdx) => (
           {groups.map((group, groupIdx) => (
             <div key={group.categoryName}>
             <div key={group.categoryName}>
-              <div className='bg-muted/30 border-border/60 border-b px-4 py-2 sm:px-5'>
+              <div className='bg-muted/30 border-border/60 border-b px-3 py-2 sm:px-5'>
                 <div className='flex items-center gap-2'>
                 <div className='flex items-center gap-2'>
                   <h4 className='text-muted-foreground text-xs font-semibold tracking-wider uppercase'>
                   <h4 className='text-muted-foreground text-xs font-semibold tracking-wider uppercase'>
                     {group.categoryName}
                     {group.categoryName}
@@ -120,7 +120,7 @@ export function UptimePanel() {
                   <div
                   <div
                     key={monitor.name}
                     key={monitor.name}
                     className={cn(
                     className={cn(
-                      'hover:bg-muted/40 flex items-center justify-between px-4 py-2.5 transition-colors sm:px-5',
+                      'hover:bg-muted/40 flex items-center justify-between gap-2 px-3 py-2 transition-colors sm:px-5 sm:py-2.5',
                       monitorIdx < (group.monitors?.length || 0) - 1 &&
                       monitorIdx < (group.monitors?.length || 0) - 1 &&
                         'border-border/40 border-b',
                         'border-border/40 border-b',
                       groupIdx < groups.length - 1 &&
                       groupIdx < groups.length - 1 &&

+ 6 - 6
web/default/src/features/dashboard/components/ui/panel-wrapper.tsx

@@ -20,10 +20,10 @@ export function PanelWrapper(props: PanelWrapperProps) {
   if (props.loading) {
   if (props.loading) {
     return (
     return (
       <div className='overflow-hidden rounded-lg border'>
       <div className='overflow-hidden rounded-lg border'>
-        <div className='border-b px-4 py-3 sm:px-5'>
+        <div className='border-b px-3 py-2.5 sm:px-5 sm:py-3'>
           <div className='text-sm font-semibold'>{props.title}</div>
           <div className='text-sm font-semibold'>{props.title}</div>
         </div>
         </div>
-        <div className='p-4 sm:p-5'>
+        <div className='p-3 sm:p-5'>
           <Skeleton className={`w-full ${height}`} />
           <Skeleton className={`w-full ${height}`} />
         </div>
         </div>
       </div>
       </div>
@@ -33,7 +33,7 @@ export function PanelWrapper(props: PanelWrapperProps) {
   if (props.empty) {
   if (props.empty) {
     return (
     return (
       <div className='overflow-hidden rounded-lg border'>
       <div className='overflow-hidden rounded-lg border'>
-        <div className='border-b px-4 py-3 sm:px-5'>
+        <div className='border-b px-3 py-2.5 sm:px-5 sm:py-3'>
           <div className='text-sm font-semibold'>{props.title}</div>
           <div className='text-sm font-semibold'>{props.title}</div>
         </div>
         </div>
         <div
         <div
@@ -47,9 +47,9 @@ export function PanelWrapper(props: PanelWrapperProps) {
 
 
   return (
   return (
     <div className='overflow-hidden rounded-lg border'>
     <div className='overflow-hidden rounded-lg border'>
-      <div className='border-b px-4 py-3 sm:px-5'>
+      <div className='border-b px-3 py-2.5 sm:px-5 sm:py-3'>
         {props.headerActions ? (
         {props.headerActions ? (
-          <div className='flex items-center justify-between'>
+          <div className='flex items-center justify-between gap-2'>
             <div className='text-sm font-semibold'>{props.title}</div>
             <div className='text-sm font-semibold'>{props.title}</div>
             {props.headerActions}
             {props.headerActions}
           </div>
           </div>
@@ -57,7 +57,7 @@ export function PanelWrapper(props: PanelWrapperProps) {
           <div className='text-sm font-semibold'>{props.title}</div>
           <div className='text-sm font-semibold'>{props.title}</div>
         )}
         )}
       </div>
       </div>
-      <div className='p-4 sm:p-5'>{props.children}</div>
+      <div className='p-3 sm:p-5'>{props.children}</div>
     </div>
     </div>
   )
   )
 }
 }

+ 12 - 10
web/default/src/features/dashboard/components/ui/stat-card.tsx

@@ -15,13 +15,15 @@ export function StatCard(props: StatCardProps) {
   const Icon = props.icon
   const Icon = props.icon
 
 
   return (
   return (
-    <div className='group flex flex-col gap-1.5 py-3'>
-      <div className='flex items-center justify-between'>
-        <div className='text-muted-foreground flex items-center gap-2 text-xs font-medium tracking-wider uppercase'>
-          <Icon className='text-muted-foreground/60 size-3.5' />
-          {props.title}
+    <div className='group flex flex-col gap-1'>
+      <div className='flex items-start justify-between gap-1'>
+        <div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium tracking-wider uppercase sm:gap-2'>
+          <Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
+          <span className='line-clamp-2 leading-snug'>{props.title}</span>
         </div>
         </div>
-        {props.action}
+        {props.action && (
+          <div className='shrink-0'>{props.action}</div>
+        )}
       </div>
       </div>
 
 
       {props.loading ? (
       {props.loading ? (
@@ -31,19 +33,19 @@ export function StatCard(props: StatCardProps) {
         </div>
         </div>
       ) : props.error ? (
       ) : props.error ? (
         <>
         <>
-          <div className='text-muted-foreground font-mono text-2xl font-bold tracking-tight tabular-nums'>
+          <div className='text-muted-foreground mt-0.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:text-2xl'>
             --
             --
           </div>
           </div>
-          <p className='text-muted-foreground/60 text-xs'>
+          <p className='text-muted-foreground/60 hidden text-xs md:block'>
             {props.description}
             {props.description}
           </p>
           </p>
         </>
         </>
       ) : (
       ) : (
         <>
         <>
-          <div className='text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums'>
+          <div className='text-foreground mt-0.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:text-2xl'>
             {props.value}
             {props.value}
           </div>
           </div>
-          <p className='text-muted-foreground/60 text-xs'>
+          <p className='text-muted-foreground/60 hidden text-xs md:block'>
             {props.description}
             {props.description}
           </p>
           </p>
         </>
         </>

+ 11 - 12
web/default/src/features/dashboard/components/users/user-charts.tsx

@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
 import { VChart } from '@visactor/react-vchart'
 import { VChart } from '@visactor/react-vchart'
 import { Users, Loader2 } from 'lucide-react'
 import { Users, Loader2 } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import { getNormalizedDateRange, type TimeGranularity } from '@/lib/time'
+import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
 import { VCHART_OPTION } from '@/lib/vchart'
 import { VCHART_OPTION } from '@/lib/vchart'
 import { useTheme } from '@/context/theme-provider'
 import { useTheme } from '@/context/theme-provider'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Skeleton } from '@/components/ui/skeleton'
@@ -60,7 +60,7 @@ export function UserCharts() {
   const [topUserLimit, setTopUserLimit] = useState(10)
   const [topUserLimit, setTopUserLimit] = useState(10)
   const [timeRange, setTimeRange] = useState(() => {
   const [timeRange, setTimeRange] = useState(() => {
     const days = getDefaultDays(timeGranularity)
     const days = getDefaultDays(timeGranularity)
-    const { start, end } = getNormalizedDateRange(days)
+    const { start, end } = getRollingDateRange(days)
     return {
     return {
       start_timestamp: Math.floor(start.getTime() / 1000),
       start_timestamp: Math.floor(start.getTime() / 1000),
       end_timestamp: Math.floor(end.getTime() / 1000),
       end_timestamp: Math.floor(end.getTime() / 1000),
@@ -69,7 +69,7 @@ export function UserCharts() {
 
 
   const handleRangeChange = useCallback((days: number) => {
   const handleRangeChange = useCallback((days: number) => {
     setSelectedRange(days)
     setSelectedRange(days)
-    const { start, end } = getNormalizedDateRange(days)
+    const { start, end } = getRollingDateRange(days)
     setTimeRange({
     setTimeRange({
       start_timestamp: Math.floor(start.getTime() / 1000),
       start_timestamp: Math.floor(start.getTime() / 1000),
       end_timestamp: Math.floor(end.getTime() / 1000),
       end_timestamp: Math.floor(end.getTime() / 1000),
@@ -123,10 +123,9 @@ export function UserCharts() {
   )
   )
 
 
   return (
   return (
-    <div className='space-y-4'>
-      {/* Toolbar: time range presets + granularity */}
-      <div className='flex flex-wrap items-center gap-2'>
-        <div className='flex items-center gap-1.5 rounded-md border p-0.5'>
+    <div className='space-y-3'>
+      <div className='flex items-center gap-1.5 overflow-x-auto pb-1 sm:gap-2'>
+        <div className='flex shrink-0 items-center gap-1.5 rounded-md border p-0.5'>
           {TIME_RANGE_PRESETS.map((preset) => (
           {TIME_RANGE_PRESETS.map((preset) => (
             <button
             <button
               key={preset.days}
               key={preset.days}
@@ -143,7 +142,7 @@ export function UserCharts() {
           ))}
           ))}
         </div>
         </div>
 
 
-        <div className='flex items-center gap-1.5 rounded-md border p-0.5'>
+        <div className='flex shrink-0 items-center gap-1.5 rounded-md border p-0.5'>
           {TIME_GRANULARITY_OPTIONS.map((opt) => (
           {TIME_GRANULARITY_OPTIONS.map((opt) => (
             <button
             <button
               key={opt.value}
               key={opt.value}
@@ -162,7 +161,7 @@ export function UserCharts() {
           ))}
           ))}
         </div>
         </div>
 
 
-        <div className='flex items-center gap-1.5 rounded-md border p-0.5'>
+        <div className='flex shrink-0 items-center gap-1.5 rounded-md border p-0.5'>
           <span className='text-muted-foreground px-2 text-xs font-medium'>
           <span className='text-muted-foreground px-2 text-xs font-medium'>
             {t('Top Users')}
             {t('Top Users')}
           </span>
           </span>
@@ -187,7 +186,7 @@ export function UserCharts() {
         )}
         )}
       </div>
       </div>
 
 
-      <div className='grid gap-4'>
+      <div className='grid gap-3'>
         {USER_CHARTS.map((chart) => {
         {USER_CHARTS.map((chart) => {
           const spec = chartData[chart.specKey]
           const spec = chartData[chart.specKey]
 
 
@@ -196,12 +195,12 @@ export function UserCharts() {
               key={chart.value}
               key={chart.value}
               className='overflow-hidden rounded-lg border'
               className='overflow-hidden rounded-lg border'
             >
             >
-              <div className='flex w-full items-center gap-2 border-b px-4 py-3 sm:px-5'>
+              <div className='flex w-full items-center gap-2 border-b px-3 py-2 sm:px-5 sm:py-3'>
                 <Users className='text-muted-foreground/60 size-4' />
                 <Users className='text-muted-foreground/60 size-4' />
                 <div className='text-sm font-semibold'>{t(chart.labelKey)}</div>
                 <div className='text-sm font-semibold'>{t(chart.labelKey)}</div>
               </div>
               </div>
 
 
-              <div className='h-96 p-2'>
+              <div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
                 {isLoading ? (
                 {isLoading ? (
                   <Skeleton className='h-full w-full' />
                   <Skeleton className='h-full w-full' />
                 ) : (
                 ) : (

+ 2 - 6
web/default/src/features/dashboard/hooks/use-dashboard-config.tsx

@@ -76,9 +76,7 @@ export function useSummaryCardsConfig(totals: {
   return [
   return [
     {
     {
       key: 'balance',
       key: 'balance',
-      title: totals.currencyEnabled
-        ? `${t('Current Balance')} (${totals.currencyLabel})`
-        : t('Current Balance'),
+      title: t('Current Balance'),
       value: totals.remainDisplay,
       value: totals.remainDisplay,
       description: totals.currencyEnabled
       description: totals.currencyEnabled
         ? `${t('Remaining quota')} (${totals.currencyLabel})`
         ? `${t('Remaining quota')} (${totals.currencyLabel})`
@@ -87,9 +85,7 @@ export function useSummaryCardsConfig(totals: {
     },
     },
     {
     {
       key: 'usage',
       key: 'usage',
-      title: totals.currencyEnabled
-        ? `${t('Historical Usage')} (${totals.currencyLabel})`
-        : t('Historical Usage'),
+      title: t('Historical Usage'),
       value: totals.usedDisplay,
       value: totals.usedDisplay,
       description: totals.currencyEnabled
       description: totals.currencyEnabled
         ? `${t('Total consumed')} (${totals.currencyLabel})`
         ? `${t('Total consumed')} (${totals.currencyLabel})`

+ 4 - 4
web/default/src/features/dashboard/index.tsx

@@ -191,9 +191,9 @@ export function Dashboard() {
         {t(meta.descriptionKey)}
         {t(meta.descriptionKey)}
       </SectionPageLayout.Description>
       </SectionPageLayout.Description>
       <SectionPageLayout.Content>
       <SectionPageLayout.Content>
-        <div className='space-y-4'>
+        <div className='space-y-3 sm:space-y-4'>
           {activeSection !== 'overview' && (
           {activeSection !== 'overview' && (
-            <div className='flex flex-wrap items-center justify-between gap-2'>
+            <div className='flex flex-wrap items-center justify-between gap-1.5 sm:gap-2'>
               {showSectionTabs ? (
               {showSectionTabs ? (
                 <Tabs value={activeSection} onValueChange={handleSectionChange}>
                 <Tabs value={activeSection} onValueChange={handleSectionChange}>
                   <TabsList className='h-auto max-w-full flex-wrap justify-start'>
                   <TabsList className='h-auto max-w-full flex-wrap justify-start'>
@@ -208,7 +208,7 @@ export function Dashboard() {
                 <div />
                 <div />
               )}
               )}
               {modelActions != null && (
               {modelActions != null && (
-                <div className='flex shrink-0 flex-wrap items-center gap-2'>
+                <div className='flex shrink-0 flex-wrap items-center gap-1.5 sm:gap-2'>
                   {modelActions}
                   {modelActions}
                 </div>
                 </div>
               )}
               )}
@@ -217,7 +217,7 @@ export function Dashboard() {
           {activeSection === 'overview' && (
           {activeSection === 'overview' && (
             <>
             <>
               <SummaryCards />
               <SummaryCards />
-              <CardStaggerContainer className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
+              <CardStaggerContainer className='grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-2'>
                 <CardStaggerItem>
                 <CardStaggerItem>
                   <ApiInfoPanel />
                   <ApiInfoPanel />
                 </CardStaggerItem>
                 </CardStaggerItem>

+ 2 - 2
web/default/src/features/dashboard/lib/filters.ts

@@ -1,5 +1,5 @@
 import type { TimeGranularity } from '@/lib/time'
 import type { TimeGranularity } from '@/lib/time'
-import { getNormalizedDateRange } from '@/lib/time'
+import { getRollingDateRange } from '@/lib/time'
 import {
 import {
   DASHBOARD_CHART_PREFERENCES_STORAGE_KEY,
   DASHBOARD_CHART_PREFERENCES_STORAGE_KEY,
   DEFAULT_DASHBOARD_CHART_PREFERENCES,
   DEFAULT_DASHBOARD_CHART_PREFERENCES,
@@ -128,7 +128,7 @@ export function getDefaultDays(granularity?: TimeGranularity): number {
 export function buildDefaultDashboardFilters(
 export function buildDefaultDashboardFilters(
   preferences: DashboardChartPreferences = getSavedChartPreferences()
   preferences: DashboardChartPreferences = getSavedChartPreferences()
 ): DashboardFilters {
 ): DashboardFilters {
-  const { start, end } = getNormalizedDateRange(preferences.defaultTimeRangeDays)
+  const { start, end } = getRollingDateRange(preferences.defaultTimeRangeDays)
   return {
   return {
     ...EMPTY_DASHBOARD_FILTERS,
     ...EMPTY_DASHBOARD_FILTERS,
     start_timestamp: start,
     start_timestamp: start,

+ 13 - 5
web/default/src/features/keys/components/api-key-group-combobox.tsx

@@ -62,7 +62,13 @@ function GroupRatioBadge({ ratio }: { ratio: ApiKeyGroupOption['ratio'] }) {
   if (!label) return null
   if (!label) return null
 
 
   return (
   return (
-    <Badge variant='outline' className={getRatioBadgeClassName(ratio)}>
+    <Badge
+      variant='outline'
+      className={cn(
+        'max-w-24 shrink-0 truncate text-[10px] sm:max-w-none sm:text-xs',
+        getRatioBadgeClassName(ratio)
+      )}
+    >
       {label}
       {label}
     </Badge>
     </Badge>
   )
   )
@@ -110,20 +116,22 @@ export function ApiKeyGroupCombobox({
           role='combobox'
           role='combobox'
           aria-expanded={open}
           aria-expanded={open}
           disabled={disabled}
           disabled={disabled}
-          className='border-input bg-muted/40 h-auto min-h-20 w-full justify-between gap-3 rounded-lg px-4 py-3 text-start shadow-none transition-[background-color,border-color,box-shadow] duration-150 hover:bg-muted/55 hover:text-foreground active:bg-background data-[state=open]:border-ring data-[state=open]:bg-background data-[state=open]:ring-ring/20 data-[state=open]:ring-[3px]'
+          className='border-input bg-muted/40 h-auto min-h-14 w-full justify-between gap-2 rounded-lg px-3 py-2 text-start shadow-none transition-[background-color,border-color,box-shadow] duration-150 hover:bg-muted/55 hover:text-foreground active:bg-background data-[state=open]:border-ring data-[state=open]:bg-background data-[state=open]:ring-ring/20 data-[state=open]:ring-[3px] sm:min-h-20 sm:gap-3 sm:px-4 sm:py-3'
         >
         >
-          <span className='flex min-w-0 flex-1 items-center justify-between gap-3'>
+          <span className='flex min-w-0 flex-1 items-center justify-between gap-2 sm:gap-3'>
             <span className='min-w-0'>
             <span className='min-w-0'>
               <span className='block truncate font-medium'>
               <span className='block truncate font-medium'>
                 {selectedOption?.value || placeholder || t('Select a group')}
                 {selectedOption?.value || placeholder || t('Select a group')}
               </span>
               </span>
               {selectedOption?.desc && (
               {selectedOption?.desc && (
-                <span className='text-muted-foreground block truncate text-xs'>
+                <span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
                   {selectedOption.desc}
                   {selectedOption.desc}
                 </span>
                 </span>
               )}
               )}
             </span>
             </span>
-            <GroupRatioBadge ratio={selectedOption?.ratio} />
+            <span className='hidden sm:block'>
+              <GroupRatioBadge ratio={selectedOption?.ratio} />
+            </span>
           </span>
           </span>
           <ChevronsUpDown className='h-4 w-4 shrink-0 opacity-50' />
           <ChevronsUpDown className='h-4 w-4 shrink-0 opacity-50' />
         </Button>
         </Button>

+ 32 - 25
web/default/src/features/keys/components/api-keys-mutate-drawer.tsx

@@ -79,18 +79,18 @@ function ApiKeyFormSection(props: ApiKeyFormSectionProps) {
 
 
   return (
   return (
     <section className='bg-card rounded-lg border'>
     <section className='bg-card rounded-lg border'>
-      <div className='flex items-center gap-3 border-b px-4 py-3'>
-        <div className='bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-lg border'>
-          <Icon className='size-5' />
+      <div className='flex items-center gap-2.5 border-b px-3 py-2.5 sm:gap-3 sm:px-4 sm:py-3'>
+        <div className='bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border sm:size-10'>
+          <Icon className='size-4 sm:size-5' />
         </div>
         </div>
         <div className='min-w-0'>
         <div className='min-w-0'>
           <h3 className='text-sm font-medium leading-none'>{props.title}</h3>
           <h3 className='text-sm font-medium leading-none'>{props.title}</h3>
-          <p className='text-muted-foreground mt-1 text-xs'>
+          <p className='text-muted-foreground mt-0.5 text-xs sm:mt-1'>
             {props.description}
             {props.description}
           </p>
           </p>
         </div>
         </div>
       </div>
       </div>
-      <div className='space-y-4 p-4'>{props.children}</div>
+      <div className='space-y-3 p-3 sm:space-y-4 sm:p-4'>{props.children}</div>
     </section>
     </section>
   )
   )
 }
 }
@@ -254,13 +254,13 @@ export function ApiKeysMutateDrawer({
     >
     >
       <SheetContent
       <SheetContent
         side={side}
         side={side}
-        className='bg-background flex w-full gap-0 overflow-hidden p-0 sm:max-w-[620px]'
+        className='bg-background flex !h-dvh !w-screen max-w-none gap-0 overflow-hidden p-0 sm:!w-full sm:!max-w-[620px]'
       >
       >
-        <SheetHeader className='bg-background border-b px-5 py-4 text-start'>
-          <SheetTitle className='text-lg'>
+        <SheetHeader className='bg-background border-b px-4 py-3 text-start sm:px-5 sm:py-4'>
+          <SheetTitle className='text-base sm:text-lg'>
             {isUpdate ? t('Update API Key') : t('Create API Key')}
             {isUpdate ? t('Update API Key') : t('Create API Key')}
           </SheetTitle>
           </SheetTitle>
-          <SheetDescription>
+          <SheetDescription className='pr-6 text-xs sm:text-sm'>
             {isUpdate
             {isUpdate
               ? t('Update the API key by providing necessary info.')
               ? t('Update the API key by providing necessary info.')
               : t('Add a new API key by providing necessary info.')}{' '}
               : t('Add a new API key by providing necessary info.')}{' '}
@@ -271,7 +271,7 @@ export function ApiKeysMutateDrawer({
           <form
           <form
             id='api-key-form'
             id='api-key-form'
             onSubmit={form.handleSubmit(onSubmit)}
             onSubmit={form.handleSubmit(onSubmit)}
-            className='flex-1 space-y-4 overflow-y-auto px-4 py-4'
+            className='min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-3 py-3 sm:space-y-4 sm:px-4 sm:py-4'
           >
           >
             <ApiKeyFormSection
             <ApiKeyFormSection
               title={t('Basic Information')}
               title={t('Basic Information')}
@@ -319,12 +319,12 @@ export function ApiKeysMutateDrawer({
                   control={form.control}
                   control={form.control}
                   name='cross_group_retry'
                   name='cross_group_retry'
                   render={({ field }) => (
                   render={({ field }) => (
-                    <FormItem className='flex min-h-20 flex-row items-center justify-between gap-4 rounded-lg border px-4 py-3'>
+                    <FormItem className='flex min-h-16 flex-row items-center justify-between gap-3 rounded-lg border px-3 py-2.5 sm:min-h-20 sm:gap-4 sm:px-4 sm:py-3'>
                       <div className='space-y-0.5'>
                       <div className='space-y-0.5'>
                         <FormLabel className='text-sm'>
                         <FormLabel className='text-sm'>
                           {t('Cross-group retry')}
                           {t('Cross-group retry')}
                         </FormLabel>
                         </FormLabel>
-                        <FormDescription className='text-xs'>
+                        <FormDescription className='line-clamp-2 text-xs sm:line-clamp-none'>
                           {t(
                           {t(
                             'When enabled, if channels in the current group fail, it will try channels in the next group in order.'
                             'When enabled, if channels in the current group fail, it will try channels in the next group in order.'
                           )}
                           )}
@@ -353,7 +353,7 @@ export function ApiKeysMutateDrawer({
                           value={field.value}
                           value={field.value}
                           onChange={field.onChange}
                           onChange={field.onChange}
                           placeholder={t('Never expires')}
                           placeholder={t('Never expires')}
-                          className='min-w-0'
+                          className='min-w-0 [&_input[type=time]]:w-24 sm:[&_input[type=time]]:w-32'
                         />
                         />
                       </FormControl>
                       </FormControl>
                       <div className='grid grid-cols-4 gap-2 sm:flex'>
                       <div className='grid grid-cols-4 gap-2 sm:flex'>
@@ -361,7 +361,7 @@ export function ApiKeysMutateDrawer({
                           type='button'
                           type='button'
                           variant='outline'
                           variant='outline'
                           size='sm'
                           size='sm'
-                          className='px-3'
+                          className='px-2 text-xs sm:px-3 sm:text-sm'
                           onClick={() => handleSetExpiry(0, 0, 0)}
                           onClick={() => handleSetExpiry(0, 0, 0)}
                         >
                         >
                           {t('Never')}
                           {t('Never')}
@@ -370,7 +370,7 @@ export function ApiKeysMutateDrawer({
                           type='button'
                           type='button'
                           variant='outline'
                           variant='outline'
                           size='sm'
                           size='sm'
-                          className='px-3'
+                          className='px-2 text-xs sm:px-3 sm:text-sm'
                           onClick={() => handleSetExpiry(1, 0, 0)}
                           onClick={() => handleSetExpiry(1, 0, 0)}
                         >
                         >
                           {t('1 Month')}
                           {t('1 Month')}
@@ -379,7 +379,7 @@ export function ApiKeysMutateDrawer({
                           type='button'
                           type='button'
                           variant='outline'
                           variant='outline'
                           size='sm'
                           size='sm'
-                          className='px-3'
+                          className='px-2 text-xs sm:px-3 sm:text-sm'
                           onClick={() => handleSetExpiry(0, 1, 0)}
                           onClick={() => handleSetExpiry(0, 1, 0)}
                         >
                         >
                           {t('1 Day')}
                           {t('1 Day')}
@@ -388,7 +388,7 @@ export function ApiKeysMutateDrawer({
                           type='button'
                           type='button'
                           variant='outline'
                           variant='outline'
                           size='sm'
                           size='sm'
-                          className='px-3'
+                          className='px-2 text-xs sm:px-3 sm:text-sm'
                           onClick={() => handleSetExpiry(0, 0, 1)}
                           onClick={() => handleSetExpiry(0, 0, 1)}
                         >
                         >
                           {t('1 Hour')}
                           {t('1 Hour')}
@@ -470,7 +470,7 @@ export function ApiKeysMutateDrawer({
                 control={form.control}
                 control={form.control}
                 name='unlimited_quota'
                 name='unlimited_quota'
                 render={({ field }) => (
                 render={({ field }) => (
-                  <FormItem className='flex min-h-20 flex-row items-center justify-between gap-4 rounded-lg border px-4 py-3'>
+                  <FormItem className='flex min-h-16 flex-row items-center justify-between gap-3 rounded-lg border px-3 py-2.5 sm:min-h-20 sm:gap-4 sm:px-4 sm:py-3'>
                     <div className='space-y-0.5'>
                     <div className='space-y-0.5'>
                       <FormLabel className='text-sm'>
                       <FormLabel className='text-sm'>
                         {t('Unlimited Quota')}
                         {t('Unlimited Quota')}
@@ -495,10 +495,10 @@ export function ApiKeysMutateDrawer({
                 <CollapsibleTrigger asChild>
                 <CollapsibleTrigger asChild>
                   <button
                   <button
                     type='button'
                     type='button'
-                    className='hover:bg-muted/50 flex w-full items-center gap-3 px-4 py-3 text-left transition-colors'
+                    className='hover:bg-muted/50 flex w-full items-center gap-2.5 px-3 py-2.5 text-left transition-colors sm:gap-3 sm:px-4 sm:py-3'
                   >
                   >
-                    <div className='bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-lg border'>
-                      <Settings2 className='size-5' />
+                    <div className='bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border sm:size-10'>
+                      <Settings2 className='size-4 sm:size-5' />
                     </div>
                     </div>
                     <div className='min-w-0 flex-1'>
                     <div className='min-w-0 flex-1'>
                       <h3 className='text-sm font-medium leading-none'>
                       <h3 className='text-sm font-medium leading-none'>
@@ -517,7 +517,7 @@ export function ApiKeysMutateDrawer({
                   </button>
                   </button>
                 </CollapsibleTrigger>
                 </CollapsibleTrigger>
                 <CollapsibleContent>
                 <CollapsibleContent>
-                  <div className='space-y-4 border-t p-4'>
+                  <div className='space-y-3 border-t p-3 sm:space-y-4 sm:p-4'>
                     <FormField
                     <FormField
                       control={form.control}
                       control={form.control}
                       name='model_limits'
                       name='model_limits'
@@ -578,11 +578,18 @@ export function ApiKeysMutateDrawer({
             </Collapsible>
             </Collapsible>
           </form>
           </form>
         </Form>
         </Form>
-        <SheetFooter className='bg-background gap-2 border-t px-5 py-4 sm:flex-row sm:justify-end'>
+        <SheetFooter className='bg-background grid grid-cols-2 gap-2 border-t px-3 py-3 sm:flex sm:flex-row sm:justify-end sm:px-5 sm:py-4'>
           <SheetClose asChild>
           <SheetClose asChild>
-            <Button variant='outline'>{t('Close')}</Button>
+            <Button variant='outline' className='w-full sm:w-auto'>
+              {t('Close')}
+            </Button>
           </SheetClose>
           </SheetClose>
-          <Button form='api-key-form' type='submit' disabled={isSubmitting}>
+          <Button
+            form='api-key-form'
+            type='submit'
+            disabled={isSubmitting}
+            className='w-full sm:w-auto'
+          >
             {isSubmitting ? t('Saving...') : t('Save changes')}
             {isSubmitting ? t('Saving...') : t('Save changes')}
           </Button>
           </Button>
         </SheetFooter>
         </SheetFooter>

+ 138 - 13
web/default/src/features/keys/components/api-keys-table.tsx

@@ -16,7 +16,9 @@ import {
 import { useMediaQuery } from '@/hooks'
 import { useMediaQuery } from '@/hooks'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { toast } from 'sonner'
 import { toast } from 'sonner'
+import { formatQuota } from '@/lib/format'
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
+import { Database } from 'lucide-react'
 import { useTableUrlState } from '@/hooks/use-table-url-state'
 import { useTableUrlState } from '@/hooks/use-table-url-state'
 import {
 import {
   Table,
   Table,
@@ -33,16 +35,31 @@ import {
   DataTableToolbar,
   DataTableToolbar,
   TableSkeleton,
   TableSkeleton,
   TableEmpty,
   TableEmpty,
-  MobileCardList,
 } from '@/components/data-table'
 } from '@/components/data-table'
+import {
+  Empty,
+  EmptyDescription,
+  EmptyHeader,
+  EmptyMedia,
+  EmptyTitle,
+} from '@/components/ui/empty'
 import { PageFooterPortal } from '@/components/layout'
 import { PageFooterPortal } from '@/components/layout'
+import { Skeleton } from '@/components/ui/skeleton'
+import { StatusBadge } from '@/components/status-badge'
 import { getApiKeys, searchApiKeys } from '../api'
 import { getApiKeys, searchApiKeys } from '../api'
-import { API_KEY_STATUS, API_KEY_STATUS_OPTIONS, ERROR_MESSAGES } from '../constants'
+import {
+  API_KEY_STATUS,
+  API_KEY_STATUS_OPTIONS,
+  API_KEY_STATUSES,
+  ERROR_MESSAGES,
+} from '../constants'
 import { type ApiKey } from '../types'
 import { type ApiKey } from '../types'
+import { ApiKeyCell } from './api-keys-cells'
 import { useApiKeysColumns } from './api-keys-columns'
 import { useApiKeysColumns } from './api-keys-columns'
 import { ApiKeysPrimaryButtons } from './api-keys-primary-buttons'
 import { ApiKeysPrimaryButtons } from './api-keys-primary-buttons'
 import { useApiKeys } from './api-keys-provider'
 import { useApiKeys } from './api-keys-provider'
 import { DataTableBulkActions } from './data-table-bulk-actions'
 import { DataTableBulkActions } from './data-table-bulk-actions'
+import { DataTableRowActions } from './data-table-row-actions'
 
 
 const route = getRouteApi('/_authenticated/keys/')
 const route = getRouteApi('/_authenticated/keys/')
 
 
@@ -50,6 +67,123 @@ function isDisabledApiKeyRow(apiKey: ApiKey) {
   return apiKey.status !== API_KEY_STATUS.ENABLED
   return apiKey.status !== API_KEY_STATUS.ENABLED
 }
 }
 
 
+function ApiKeysMobileSkeleton() {
+  return (
+    <div className='divide-border overflow-hidden rounded-lg border'>
+      {Array.from({ length: 5 }).map((_, index) => (
+        <div
+          key={index}
+          className='space-y-2 border-b px-3 py-2.5 last:border-b-0'
+        >
+          <div className='flex items-center justify-between'>
+            <Skeleton className='h-4 w-32' />
+            <Skeleton className='h-5 w-16 rounded-full' />
+          </div>
+          <div className='flex items-center justify-between gap-3'>
+            <Skeleton className='h-7 w-44' />
+            <Skeleton className='h-8 w-16' />
+          </div>
+          <Skeleton className='h-3 w-28' />
+        </div>
+      ))}
+    </div>
+  )
+}
+
+function ApiKeysMobileList({
+  table,
+  isLoading,
+}: {
+  table: ReturnType<typeof useReactTable<ApiKey>>
+  isLoading: boolean
+}) {
+  const { t } = useTranslation()
+  const rows = table.getRowModel().rows
+
+  if (isLoading) return <ApiKeysMobileSkeleton />
+
+  if (!rows.length) {
+    return (
+      <div className='rounded-lg border p-8'>
+        <Empty className='border-none p-0'>
+          <EmptyHeader>
+            <EmptyMedia variant='icon'>
+              <Database className='size-6' />
+            </EmptyMedia>
+            <EmptyTitle>{t('No API Keys Found')}</EmptyTitle>
+            <EmptyDescription>
+              {t(
+                'No API keys available. Create your first API key to get started.'
+              )}
+            </EmptyDescription>
+          </EmptyHeader>
+        </Empty>
+      </div>
+    )
+  }
+
+  return (
+    <div className='divide-border overflow-hidden rounded-lg border'>
+      {rows.map((row) => {
+        const apiKey = row.original
+        const statusConfig = API_KEY_STATUSES[apiKey.status]
+        const total = apiKey.used_quota + apiKey.remain_quota
+
+        return (
+          <div
+            key={row.id}
+            className={cn(
+              'bg-card space-y-2.5 border-b px-3 py-2.5 last:border-b-0',
+              isDisabledApiKeyRow(apiKey) && DISABLED_ROW_MOBILE
+            )}
+          >
+            <div className='flex items-start justify-between gap-3'>
+              <div className='min-w-0'>
+                <div className='truncate text-sm font-semibold'>
+                  {apiKey.name}
+                </div>
+                <div className='text-muted-foreground text-[11px]'>
+                  {t('API Key')}
+                </div>
+              </div>
+              {statusConfig && (
+                <StatusBadge
+                  label={t(statusConfig.label)}
+                  variant={statusConfig.variant}
+                  showDot={statusConfig.showDot}
+                  copyable={false}
+                />
+              )}
+            </div>
+
+            <div className='flex min-w-0 items-center justify-between gap-2'>
+              <div className='min-w-0 flex-1 [&_button:first-child]:max-w-full [&_button:first-child]:truncate [&_button:first-child]:px-0'>
+                <ApiKeyCell apiKey={apiKey} />
+              </div>
+              <DataTableRowActions row={row} />
+            </div>
+
+            <div className='flex items-center justify-between gap-2 text-xs'>
+              <span className='text-muted-foreground'>{t('Quota')}</span>
+              {apiKey.unlimited_quota ? (
+                <span className='font-medium'>{t('Unlimited')}</span>
+              ) : (
+                <span className='font-medium tabular-nums'>
+                  {formatQuota(apiKey.remain_quota)}
+                  <span className='text-muted-foreground font-normal'>
+                    {' / '}
+                    {formatQuota(total)}
+                  </span>
+                </span>
+              )}
+            </div>
+          </div>
+        )
+      })}
+    </div>
+  )
+}
+
 export function ApiKeysTable() {
 export function ApiKeysTable() {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { refreshTrigger } = useApiKeys()
   const { refreshTrigger } = useApiKeys()
@@ -166,7 +300,7 @@ export function ApiKeysTable() {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         <div className='flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between'>
         <div className='flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between'>
           <ApiKeysPrimaryButtons />
           <ApiKeysPrimaryButtons />
           <div className='min-w-0 sm:flex sm:justify-end'>
           <div className='min-w-0 sm:flex sm:justify-end'>
@@ -184,18 +318,9 @@ export function ApiKeysTable() {
           </div>
           </div>
         </div>
         </div>
         {isMobile ? (
         {isMobile ? (
-          <MobileCardList
+          <ApiKeysMobileList
             table={table}
             table={table}
             isLoading={isLoading}
             isLoading={isLoading}
-            emptyTitle={t('No API Keys Found')}
-            emptyDescription={t(
-              'No API keys available. Create your first API key to get started.'
-            )}
-            getRowClassName={(row) =>
-              isDisabledApiKeyRow(row.original)
-                ? DISABLED_ROW_MOBILE
-                : undefined
-            }
           />
           />
         ) : (
         ) : (
           <div
           <div

+ 2 - 2
web/default/src/features/models/components/deployments-table.tsx

@@ -72,7 +72,7 @@ export function DeploymentsTable() {
       pageKey: 'dPage',
       pageKey: 'dPage',
       pageSizeKey: 'dPageSize',
       pageSizeKey: 'dPageSize',
       defaultPage: 1,
       defaultPage: 1,
-      defaultPageSize: 10,
+      defaultPageSize: isMobile ? 8 : 10,
     },
     },
     globalFilter: { enabled: true, key: 'dFilter' },
     globalFilter: { enabled: true, key: 'dFilter' },
     columnFilters: [
     columnFilters: [
@@ -229,7 +229,7 @@ export function DeploymentsTable() {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         <DataTableToolbar
         <DataTableToolbar
           table={table}
           table={table}
           searchPlaceholder={t('Search deployments...')}
           searchPlaceholder={t('Search deployments...')}

+ 7 - 7
web/default/src/features/models/components/dialogs/update-config-dialog.tsx

@@ -195,7 +195,7 @@ export function UpdateConfigDialog({
 
 
   return (
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'>
+      <DialogContent className='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{title}</DialogTitle>
           <DialogTitle>{title}</DialogTitle>
         </DialogHeader>
         </DialogHeader>
@@ -205,14 +205,14 @@ export function UpdateConfigDialog({
             <Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
             <Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
           </div>
           </div>
         ) : (
         ) : (
-          <div className='max-h-[72vh] overflow-y-auto py-2 pr-1'>
+          <div className='max-h-[calc(100dvh-8.5rem)] overflow-y-auto py-2 pr-1 sm:max-h-[72vh]'>
             <Form {...form}>
             <Form {...form}>
               <form
               <form
                 onSubmit={form.handleSubmit(onSubmit)}
                 onSubmit={form.handleSubmit(onSubmit)}
                 autoComplete='off'
                 autoComplete='off'
                 className='space-y-4'
                 className='space-y-4'
               >
               >
-                <div className='grid gap-4 md:grid-cols-2'>
+                <div className='grid gap-3 md:grid-cols-2 md:gap-4'>
                   <FormField
                   <FormField
                     control={form.control}
                     control={form.control}
                     name='image_url'
                     name='image_url'
@@ -262,7 +262,7 @@ export function UpdateConfigDialog({
                   />
                   />
                 </div>
                 </div>
 
 
-                <div className='grid gap-4 md:grid-cols-2'>
+                <div className='grid gap-3 md:grid-cols-2 md:gap-4'>
                   <FormField
                   <FormField
                     control={form.control}
                     control={form.control}
                     name='entrypoint'
                     name='entrypoint'
@@ -313,7 +313,7 @@ export function UpdateConfigDialog({
                     {t('Registry (optional)')}
                     {t('Registry (optional)')}
                   </CollapsibleTrigger>
                   </CollapsibleTrigger>
                   <CollapsibleContent>
                   <CollapsibleContent>
-                    <div className='mt-3 grid gap-4 md:grid-cols-2'>
+                    <div className='mt-3 grid gap-3 md:grid-cols-2 md:gap-4'>
                       <FormField
                       <FormField
                         control={form.control}
                         control={form.control}
                         name='registry_username'
                         name='registry_username'
@@ -353,7 +353,7 @@ export function UpdateConfigDialog({
                     {t('Environment variables')}
                     {t('Environment variables')}
                   </CollapsibleTrigger>
                   </CollapsibleTrigger>
                   <CollapsibleContent>
                   <CollapsibleContent>
-                    <div className='mt-3 grid gap-4 md:grid-cols-2'>
+                    <div className='mt-3 grid gap-3 md:grid-cols-2 md:gap-4'>
                       <FormField
                       <FormField
                         control={form.control}
                         control={form.control}
                         name='env_json'
                         name='env_json'
@@ -394,7 +394,7 @@ export function UpdateConfigDialog({
                   </CollapsibleContent>
                   </CollapsibleContent>
                 </Collapsible>
                 </Collapsible>
 
 
-                <DialogFooter className='pt-2'>
+                <DialogFooter className='grid grid-cols-2 gap-2 pt-2 sm:flex'>
                   <Button
                   <Button
                     type='button'
                     type='button'
                     variant='outline'
                     variant='outline'

+ 5 - 5
web/default/src/features/models/components/dialogs/view-details-dialog.tsx

@@ -99,18 +99,18 @@ export function ViewDetailsDialog({
 
 
   return (
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'>
+      <DialogContent className='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t('Deployment details')}</DialogTitle>
           <DialogTitle>{t('Deployment details')}</DialogTitle>
         </DialogHeader>
         </DialogHeader>
 
 
-        <div className='max-h-[72vh] space-y-4 overflow-y-auto py-2 pr-1'>
-          <div className='flex flex-wrap items-center justify-between gap-2'>
+        <div className='max-h-[calc(100dvh-8.5rem)] space-y-3 overflow-y-auto py-2 pr-1 sm:max-h-[72vh] sm:space-y-4'>
+            <div className='flex flex-wrap items-center justify-between gap-2'>
             <div className='text-muted-foreground text-sm'>
             <div className='text-muted-foreground text-sm'>
               {t('Deployment ID')}:{' '}
               {t('Deployment ID')}:{' '}
               <span className='font-mono'>{deploymentId}</span>
               <span className='font-mono'>{deploymentId}</span>
             </div>
             </div>
-            <div className='flex items-center gap-2'>
+            <div className='grid grid-cols-2 gap-2 sm:flex sm:items-center'>
               <Button variant='outline' size='sm' onClick={handleCopyId}>
               <Button variant='outline' size='sm' onClick={handleCopyId}>
                 <Copy className='mr-2 h-4 w-4' />
                 <Copy className='mr-2 h-4 w-4' />
                 {t('Copy')}
                 {t('Copy')}
@@ -252,7 +252,7 @@ export function ViewDetailsDialog({
         </div>
         </div>
 
 
         <DialogFooter>
         <DialogFooter>
-          <Button variant='outline' onClick={() => onOpenChange(false)}>
+          <Button variant='outline' onClick={() => onOpenChange(false)} className='w-full sm:w-auto'>
             {t('Close')}
             {t('Close')}
           </Button>
           </Button>
         </DialogFooter>
         </DialogFooter>

+ 6 - 6
web/default/src/features/models/components/dialogs/view-logs-dialog.tsx

@@ -124,7 +124,7 @@ export function ViewLogsDialog({
 
 
   return (
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className='flex h-[80vh] max-w-4xl flex-col'>
+      <DialogContent className='flex h-[calc(100dvh-2rem)] flex-col max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:h-[80vh] sm:max-w-4xl'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle className='flex items-center gap-2'>
           <DialogTitle className='flex items-center gap-2'>
             <Terminal className='h-5 w-5' />
             <Terminal className='h-5 w-5' />
@@ -132,11 +132,11 @@ export function ViewLogsDialog({
           </DialogTitle>
           </DialogTitle>
         </DialogHeader>
         </DialogHeader>
 
 
-        <div className='mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
+        <div className='mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3'>
           <div className='text-muted-foreground text-sm'>
           <div className='text-muted-foreground text-sm'>
             {t('Deployment ID')}: {deploymentId}
             {t('Deployment ID')}: {deploymentId}
           </div>
           </div>
-          <div className='flex flex-wrap items-center gap-2'>
+          <div className='grid grid-cols-2 gap-2 sm:flex sm:flex-wrap sm:items-center'>
             <Button
             <Button
               variant='outline'
               variant='outline'
               size='sm'
               size='sm'
@@ -162,14 +162,14 @@ export function ViewLogsDialog({
               <Download className='mr-2 h-4 w-4' />
               <Download className='mr-2 h-4 w-4' />
               {t('Download')}
               {t('Download')}
             </Button>
             </Button>
-            <div className='flex items-center gap-2 rounded-md border px-3 py-1.5'>
+            <div className='col-span-2 flex items-center justify-between gap-2 rounded-md border px-3 py-1.5 sm:col-span-1'>
               <span className='text-xs'>{t('Auto refresh')}</span>
               <span className='text-xs'>{t('Auto refresh')}</span>
               <Switch checked={autoRefresh} onCheckedChange={setAutoRefresh} />
               <Switch checked={autoRefresh} onCheckedChange={setAutoRefresh} />
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className='mb-3 grid gap-3 sm:grid-cols-2'>
+        <div className='mb-3 grid gap-2 sm:grid-cols-2 sm:gap-3'>
           <div className='space-y-1'>
           <div className='space-y-1'>
             <div className='text-muted-foreground text-xs'>
             <div className='text-muted-foreground text-xs'>
               {t('Container')}
               {t('Container')}
@@ -234,7 +234,7 @@ export function ViewLogsDialog({
 
 
         <div
         <div
           ref={scrollRef}
           ref={scrollRef}
-          className='flex-1 overflow-auto rounded-md border bg-black p-4'
+          className='flex-1 overflow-auto rounded-md border bg-black p-3 sm:p-4'
           onScroll={(e) => {
           onScroll={(e) => {
             const target = e.target as HTMLDivElement
             const target = e.target as HTMLDivElement
             const isAtBottom =
             const isAtBottom =

+ 4 - 4
web/default/src/features/models/components/drawers/model-mutate-drawer.tsx

@@ -601,8 +601,8 @@ export function ModelMutateDrawer({
 
 
   return (
   return (
     <Sheet open={open} onOpenChange={onOpenChange}>
     <Sheet open={open} onOpenChange={onOpenChange}>
-      <SheetContent className='flex w-full flex-col sm:max-w-2xl'>
-        <SheetHeader className='text-start'>
+      <SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-2xl'>
+        <SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
           <SheetTitle>
           <SheetTitle>
             {isEditing ? t('Edit Model') : t('Create Model')}
             {isEditing ? t('Edit Model') : t('Create Model')}
           </SheetTitle>
           </SheetTitle>
@@ -621,7 +621,7 @@ export function ModelMutateDrawer({
             onSubmit={form.handleSubmit(
             onSubmit={form.handleSubmit(
               onSubmit as Parameters<typeof form.handleSubmit>[0]
               onSubmit as Parameters<typeof form.handleSubmit>[0]
             )}
             )}
-            className='flex-1 space-y-6 overflow-y-auto px-4'
+            className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4'
           >
           >
             {/* Basic Information */}
             {/* Basic Information */}
             <div className='space-y-4'>
             <div className='space-y-4'>
@@ -1232,7 +1232,7 @@ export function ModelMutateDrawer({
           </form>
           </form>
         </Form>
         </Form>
 
 
-        <SheetFooter className='gap-2'>
+        <SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
           <SheetClose asChild>
           <SheetClose asChild>
             <Button variant='outline' disabled={isSubmitting}>
             <Button variant='outline' disabled={isSubmitting}>
               {t('Cancel')}
               {t('Cancel')}

+ 5 - 5
web/default/src/features/models/components/drawers/prefill-group-form-drawer.tsx

@@ -161,8 +161,8 @@ export function PrefillGroupFormDrawer({
 
 
   return (
   return (
     <Sheet open={open} onOpenChange={handleOpenChange}>
     <Sheet open={open} onOpenChange={handleOpenChange}>
-      <SheetContent className='flex w-full flex-col sm:max-w-2xl'>
-        <SheetHeader className='text-start'>
+      <SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-2xl'>
+        <SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
           <SheetTitle>
           <SheetTitle>
             {isEdit ? t('Edit Prefill Group') : t('Create Prefill Group')}
             {isEdit ? t('Edit Prefill Group') : t('Create Prefill Group')}
           </SheetTitle>
           </SheetTitle>
@@ -177,7 +177,7 @@ export function PrefillGroupFormDrawer({
           <form
           <form
             id='prefill-group-form'
             id='prefill-group-form'
             onSubmit={form.handleSubmit(handleSubmit)}
             onSubmit={form.handleSubmit(handleSubmit)}
-            className='flex-1 space-y-6 overflow-y-auto px-4'
+            className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4'
           >
           >
             <div className='space-y-4'>
             <div className='space-y-4'>
               <div className='space-y-1'>
               <div className='space-y-1'>
@@ -286,7 +286,7 @@ export function PrefillGroupFormDrawer({
                 )}
                 )}
               />
               />
 
 
-              <div className='space-y-2 rounded-lg border p-4'>
+              <div className='space-y-2 rounded-lg border p-3 sm:p-4'>
                 <div className='flex items-center gap-2'>
                 <div className='flex items-center gap-2'>
                   <h4 className='text-sm font-medium'>{t('Project')}</h4>
                   <h4 className='text-sm font-medium'>{t('Project')}</h4>
                   <StatusBadge
                   <StatusBadge
@@ -343,7 +343,7 @@ export function PrefillGroupFormDrawer({
           </form>
           </form>
         </Form>
         </Form>
 
 
-        <SheetFooter className='gap-2'>
+        <SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
           <SheetClose asChild>
           <SheetClose asChild>
             <Button type='button' variant='outline' disabled={isSaving}>
             <Button type='button' variant='outline' disabled={isSaving}>
               {t('Cancel')}
               {t('Cancel')}

+ 5 - 2
web/default/src/features/models/components/models-table.tsx

@@ -67,7 +67,10 @@ export function ModelsTable() {
   } = useTableUrlState({
   } = useTableUrlState({
     search: route.useSearch(),
     search: route.useSearch(),
     navigate: route.useNavigate(),
     navigate: route.useNavigate(),
-    pagination: { defaultPage: 1, defaultPageSize: DEFAULT_PAGE_SIZE },
+    pagination: {
+      defaultPage: 1,
+      defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE,
+    },
     globalFilter: { enabled: true, key: 'filter' },
     globalFilter: { enabled: true, key: 'filter' },
     columnFilters: [
     columnFilters: [
       { columnId: 'status', searchKey: 'status', type: 'array' },
       { columnId: 'status', searchKey: 'status', type: 'array' },
@@ -217,7 +220,7 @@ export function ModelsTable() {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         <DataTableToolbar
         <DataTableToolbar
           table={table}
           table={table}
           searchPlaceholder={t('Filter by model name...')}
           searchPlaceholder={t('Filter by model name...')}

+ 64 - 4
web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx

@@ -206,8 +206,8 @@ export function DynamicPricingBreakdown({
   })
   })
 
 
   return (
   return (
-    <section className='min-w-0 py-4'>
-      <div className='mb-4 flex items-start gap-2'>
+    <section className='min-w-0 py-3 sm:py-4'>
+      <div className='mb-3 flex items-start gap-2 sm:mb-4'>
         <span className='mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
         <span className='mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
           <TagIcon className='size-3.5' />
           <TagIcon className='size-3.5' />
         </span>
         </span>
@@ -222,11 +222,71 @@ export function DynamicPricingBreakdown({
       </div>
       </div>
 
 
       {hasTiers && (
       {hasTiers && (
-        <div className='mb-4'>
+        <div className='mb-3 sm:mb-4'>
           <div className='text-foreground mb-2 text-sm font-semibold'>
           <div className='text-foreground mb-2 text-sm font-semibold'>
             {t('Tiered price table')}
             {t('Tiered price table')}
           </div>
           </div>
-          <div className='-mx-4 max-w-[calc(100%+2rem)] overflow-x-auto sm:mx-0 sm:max-w-full'>
+          <div className='space-y-1.5 sm:hidden'>
+            {tiers.map((tier, i) => {
+              const condSummary = formatConditionSummary(tier.conditions, t)
+              const isMatched =
+                matchedTierLabel != null &&
+                matchedTierLabel !== '' &&
+                tier.label === matchedTierLabel
+              return (
+                <div
+                  key={`tier-mobile-${i}`}
+                  className={cn(
+                    'rounded-md border p-2',
+                    isMatched &&
+                      'border-emerald-500/40 bg-emerald-500/10'
+                  )}
+                >
+                  <div className='mb-1.5 flex flex-wrap items-center gap-1.5'>
+                    <Badge
+                      variant='secondary'
+                      className='bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
+                    >
+                      {tier.label || t('Default')}
+                    </Badge>
+                    {isMatched && (
+                      <Badge
+                        variant='secondary'
+                        className='bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
+                      >
+                        {t('Matched')}
+                      </Badge>
+                    )}
+                  </div>
+                  {condSummary && (
+                    <div className='text-muted-foreground mb-1.5 text-xs'>
+                      {condSummary}
+                    </div>
+                  )}
+                  <div className='grid grid-cols-2 gap-x-3 gap-y-1.5'>
+                    {visiblePriceFields.map((v) => {
+                      const value = Number(
+                        tier[v.field as string as keyof ParsedTier] || 0
+                      )
+                      return (
+                        <div key={v.field} className='min-w-0'>
+                          <div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
+                            {t(v.shortLabel)}
+                          </div>
+                          <div className='truncate font-mono text-sm font-semibold'>
+                            {value > 0
+                              ? `${symbol}${(value * rate).toFixed(4)}`
+                              : '-'}
+                          </div>
+                        </div>
+                      )
+                    })}
+                  </div>
+                </div>
+              )
+            })}
+          </div>
+          <div className='hidden overflow-x-auto sm:block'>
             <Table className='text-sm'>
             <Table className='text-sm'>
               <TableHeader>
               <TableHeader>
                 <TableRow className='hover:bg-transparent'>
                 <TableRow className='hover:bg-transparent'>

+ 6 - 6
web/default/src/features/pricing/components/filter-bar.tsx

@@ -620,16 +620,16 @@ export function FilterBar(props: FilterBarProps) {
       <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
       <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
         <SheetContent
         <SheetContent
           side='right'
           side='right'
-          className='flex w-full flex-col overflow-hidden p-0 sm:max-w-md'
+          className='flex h-dvh w-full flex-col overflow-hidden p-0 sm:max-w-md'
         >
         >
-          <SheetHeader className='border-b px-6 py-4'>
+          <SheetHeader className='border-b px-4 py-3 sm:px-6 sm:py-4'>
             <SheetTitle>{t('Filters')}</SheetTitle>
             <SheetTitle>{t('Filters')}</SheetTitle>
             <SheetDescription className='sr-only'>
             <SheetDescription className='sr-only'>
               {t('Filter models by type, endpoint, vendor, group and tags')}
               {t('Filter models by type, endpoint, vendor, group and tags')}
             </SheetDescription>
             </SheetDescription>
           </SheetHeader>
           </SheetHeader>
 
 
-          <div className='flex-1 space-y-6 overflow-y-auto px-6 py-5'>
+          <div className='flex-1 space-y-4 overflow-y-auto px-4 py-4 sm:space-y-6 sm:px-6 sm:py-5'>
             <MobileFilterGroup
             <MobileFilterGroup
               title={t('Pricing Type')}
               title={t('Pricing Type')}
               value={props.quotaTypeFilter}
               value={props.quotaTypeFilter}
@@ -671,7 +671,7 @@ export function FilterBar(props: FilterBarProps) {
               <h3 className='text-foreground mb-3 text-sm font-semibold'>
               <h3 className='text-foreground mb-3 text-sm font-semibold'>
                 {t('Display Options')}
                 {t('Display Options')}
               </h3>
               </h3>
-              <div className='space-y-4'>
+              <div className='space-y-3 sm:space-y-4'>
                 <div className='space-y-2'>
                 <div className='space-y-2'>
                   <p className='text-muted-foreground text-xs'>
                   <p className='text-muted-foreground text-xs'>
                     {t('Price display')}
                     {t('Price display')}
@@ -704,8 +704,8 @@ export function FilterBar(props: FilterBarProps) {
             </div>
             </div>
           </div>
           </div>
 
 
-          <SheetFooter className='border-t px-6 py-4'>
-            <div className='flex w-full gap-3'>
+          <SheetFooter className='border-t px-4 py-3 sm:px-6 sm:py-4'>
+            <div className='grid w-full grid-cols-2 gap-2 sm:flex sm:gap-3'>
               {props.hasActiveFilters && (
               {props.hasActiveFilters && (
                 <Button
                 <Button
                   variant='outline'
                   variant='outline'

+ 3 - 3
web/default/src/features/pricing/components/model-card.tsx

@@ -68,7 +68,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
   return (
   return (
     <div
     <div
       className={cn(
       className={cn(
-        'group flex flex-col rounded-xl border p-4 transition-colors sm:p-5',
+        'group flex flex-col rounded-xl border p-3 transition-colors sm:p-5',
         'hover:bg-muted/20'
         'hover:bg-muted/20'
       )}
       )}
     >
     >
@@ -175,12 +175,12 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
       </div>
       </div>
 
 
       {/* Description */}
       {/* Description */}
-      <p className='text-muted-foreground mt-3 line-clamp-1 flex-1 text-[13px] leading-relaxed sm:mt-4 sm:line-clamp-2 sm:min-h-[2.5rem]'>
+      <p className='text-muted-foreground mt-2 line-clamp-1 flex-1 text-[13px] leading-relaxed sm:mt-4 sm:line-clamp-2 sm:min-h-[2.5rem]'>
         {props.model.description || t('No description available.')}
         {props.model.description || t('No description available.')}
       </p>
       </p>
 
 
       {/* Footer row 1: group + billing type */}
       {/* Footer row 1: group + billing type */}
-      <div className='mt-3 flex flex-wrap items-center gap-x-2 gap-y-1 sm:mt-4'>
+      <div className='mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 sm:mt-4'>
         {primaryGroup && (
         {primaryGroup && (
           <span className='text-muted-foreground text-xs font-medium'>
           <span className='text-muted-foreground text-xs font-medium'>
             {primaryGroup} {t('Groups')}
             {primaryGroup} {t('Groups')}

+ 2 - 2
web/default/src/features/pricing/components/model-details.tsx

@@ -783,13 +783,13 @@ export function ModelDetailsDrawer(props: ModelDetailsDrawerProps) {
     <Sheet open={open} onOpenChange={onOpenChange}>
     <Sheet open={open} onOpenChange={onOpenChange}>
       <SheetContent
       <SheetContent
         side='right'
         side='right'
-        className='flex w-full overflow-hidden p-0 sm:max-w-2xl xl:max-w-3xl'
+        className='flex h-dvh w-full overflow-hidden p-0 sm:max-w-2xl xl:max-w-3xl'
       >
       >
         <SheetHeader className='sr-only'>
         <SheetHeader className='sr-only'>
           <SheetTitle>{props.model.model_name}</SheetTitle>
           <SheetTitle>{props.model.model_name}</SheetTitle>
           <SheetDescription>{t('Model details')}</SheetDescription>
           <SheetDescription>{t('Model details')}</SheetDescription>
         </SheetHeader>
         </SheetHeader>
-        <div className='flex-1 overflow-y-auto px-5 pt-12 pb-6 sm:px-6'>
+        <div className='flex-1 overflow-y-auto px-4 pt-11 pb-5 sm:px-6 sm:pt-12 sm:pb-6'>
           <ModelDetailsContent {...contentProps} />
           <ModelDetailsContent {...contentProps} />
         </div>
         </div>
       </SheetContent>
       </SheetContent>

+ 3 - 3
web/default/src/features/pricing/components/pricing-toolbar.tsx

@@ -259,15 +259,15 @@ export function PricingToolbar(props: PricingToolbarProps) {
       <Sheet open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
       <Sheet open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
         <SheetContent
         <SheetContent
           side='right'
           side='right'
-          className='flex w-full flex-col overflow-hidden p-0 sm:max-w-md'
+          className='flex h-dvh w-full flex-col overflow-hidden p-0 sm:max-w-md'
         >
         >
-          <SheetHeader className='border-b px-6 py-4'>
+          <SheetHeader className='border-b px-4 py-3 sm:px-6 sm:py-4'>
             <SheetTitle>{t('Filter')}</SheetTitle>
             <SheetTitle>{t('Filter')}</SheetTitle>
             <SheetDescription>
             <SheetDescription>
               {t('Filter models by provider, group, type, endpoint, and tags.')}
               {t('Filter models by provider, group, type, endpoint, and tags.')}
             </SheetDescription>
             </SheetDescription>
           </SheetHeader>
           </SheetHeader>
-          <div className='flex-1 overflow-y-auto p-4'>
+          <div className='flex-1 overflow-y-auto p-3 sm:p-4'>
             <PricingSidebar
             <PricingSidebar
               quotaTypeFilter={props.quotaTypeFilter}
               quotaTypeFilter={props.quotaTypeFilter}
               endpointTypeFilter={props.endpointTypeFilter}
               endpointTypeFilter={props.endpointTypeFilter}

+ 5 - 5
web/default/src/features/pricing/index.tsx

@@ -129,7 +129,7 @@ export function Pricing() {
   if (isLoading) {
   if (isLoading) {
     return (
     return (
       <PublicLayout showMainContainer={false}>
       <PublicLayout showMainContainer={false}>
-        <div className='mx-auto w-full max-w-[1800px] px-4 pt-20 pb-10 sm:px-6 xl:px-8'>
+        <div className='mx-auto w-full max-w-[1800px] px-3 pt-16 pb-8 sm:px-6 sm:pt-20 sm:pb-10 xl:px-8'>
           <LoadingSkeleton viewMode={viewMode} />
           <LoadingSkeleton viewMode={viewMode} />
         </div>
         </div>
       </PublicLayout>
       </PublicLayout>
@@ -152,15 +152,15 @@ export function Pricing() {
             WebkitMaskImage: 'linear-gradient(to bottom, black 40%, transparent 100%)',
             WebkitMaskImage: 'linear-gradient(to bottom, black 40%, transparent 100%)',
           }}
           }}
         />
         />
-        <PageTransition className='relative mx-auto w-full max-w-[1800px] px-4 pt-20 pb-10 sm:px-6 xl:px-8'>
-          <header className='mx-auto mb-8 max-w-3xl pt-8 text-center sm:mb-10 sm:pt-10'>
+        <PageTransition className='relative mx-auto w-full max-w-[1800px] px-3 pt-16 pb-8 sm:px-6 sm:pt-20 sm:pb-10 xl:px-8'>
+          <header className='mx-auto mb-5 max-w-3xl pt-5 text-center sm:mb-10 sm:pt-10'>
             <p className='text-muted-foreground mb-3 text-xs font-medium tracking-widest uppercase'>
             <p className='text-muted-foreground mb-3 text-xs font-medium tracking-widest uppercase'>
               {t('Models Directory')}
               {t('Models Directory')}
             </p>
             </p>
             <h1 className='text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.15] font-bold tracking-tight'>
             <h1 className='text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.15] font-bold tracking-tight'>
               {t('Model Square')}
               {t('Model Square')}
             </h1>
             </h1>
-            <p className='text-muted-foreground/80 mt-4 text-sm sm:text-base'>
+            <p className='text-muted-foreground/80 mt-3 text-sm sm:mt-4 sm:text-base'>
               {t('This site currently has {{count}} models enabled', {
               {t('This site currently has {{count}} models enabled', {
                 count: models?.length || 0,
                 count: models?.length || 0,
               })}
               })}
@@ -175,7 +175,7 @@ export function Pricing() {
               onChange={setSearchInput}
               onChange={setSearchInput}
               onClear={clearSearch}
               onClear={clearSearch}
               placeholder={t('Search model name, provider, endpoint, or tag...')}
               placeholder={t('Search model name, provider, endpoint, or tag...')}
-              className='mx-auto mt-6 max-w-2xl'
+              className='mx-auto mt-4 max-w-2xl sm:mt-6'
             />
             />
           </header>
           </header>
 
 

+ 8 - 0
web/default/src/features/profile/api.ts

@@ -41,6 +41,14 @@ export async function updateUserSettings(
   return res.data
   return res.data
 }
 }
 
 
+/**
+ * Update interface language preference
+ */
+export async function updateUserLanguage(language: string): Promise<ApiResponse> {
+  const res = await api.put('/api/user/self', { language })
+  return res.data
+}
+
 /**
 /**
  * Delete user account
  * Delete user account
  */
  */

+ 136 - 0
web/default/src/features/profile/components/language-preferences-card.tsx

@@ -0,0 +1,136 @@
+import { useEffect, useMemo, useState } from 'react'
+import { Languages, Loader2 } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { toast } from 'sonner'
+import { useAuthStore } from '@/stores/auth-store'
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select'
+import { TitledCard } from '@/components/ui/titled-card'
+import { updateUserLanguage } from '../api'
+import { parseUserSettings } from '../lib'
+import type { UserProfile } from '../types'
+
+const LANGUAGE_OPTIONS = [
+  { value: 'zh', label: '简体中文' },
+  { value: 'en', label: 'English' },
+  { value: 'fr', label: 'Français' },
+  { value: 'ru', label: 'Русский' },
+  { value: 'ja', label: '日本語' },
+  { value: 'vi', label: 'Tiếng Việt' },
+] as const
+
+function normalizeLanguage(value?: string | null): string {
+  if (!value) return 'en'
+  const normalized = value.trim().replace(/_/g, '-').toLowerCase()
+  if (normalized.startsWith('zh')) return 'zh'
+  return LANGUAGE_OPTIONS.some((lang) => lang.value === normalized)
+    ? normalized
+    : 'en'
+}
+
+type LanguagePreferencesCardProps = {
+  profile: UserProfile | null
+  onProfileUpdate: () => void
+}
+
+export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
+  const { t, i18n } = useTranslation()
+  const { auth } = useAuthStore()
+  const [saving, setSaving] = useState(false)
+
+  const savedLanguage = useMemo(() => {
+    const settings = parseUserSettings(props.profile?.setting)
+    return normalizeLanguage(settings.language || i18n.language)
+  }, [props.profile?.setting, i18n.language])
+
+  const [currentLanguage, setCurrentLanguage] = useState(savedLanguage)
+
+  useEffect(() => {
+    setCurrentLanguage(savedLanguage)
+  }, [savedLanguage])
+
+  const handleLanguageChange = async (language: string) => {
+    const nextLanguage = normalizeLanguage(language)
+    if (nextLanguage === currentLanguage) return
+
+    const previousLanguage = currentLanguage
+    setCurrentLanguage(nextLanguage)
+    setSaving(true)
+    await i18n.changeLanguage(nextLanguage)
+
+    try {
+      const response = await updateUserLanguage(nextLanguage)
+      if (!response.success) {
+        throw new Error(response.message || t('Failed to update settings'))
+      }
+
+      if (auth.user) {
+        const existingSetting =
+          typeof auth.user.setting === 'string'
+            ? parseUserSettings(auth.user.setting)
+            : (auth.user.setting ?? {})
+        auth.setUser({
+          ...auth.user,
+          setting: JSON.stringify({
+            ...existingSetting,
+            language: nextLanguage,
+          }),
+        })
+      }
+
+      props.onProfileUpdate()
+      toast.success(t('Language preference saved'))
+    } catch (_error) {
+      setCurrentLanguage(previousLanguage)
+      await i18n.changeLanguage(previousLanguage)
+      toast.error(t('Failed to update settings'))
+    } finally {
+      setSaving(false)
+    }
+  }
+
+  return (
+    <TitledCard
+      title={t('Language Preferences')}
+      description={t('Set the language used across the interface')}
+      icon={<Languages className='h-4 w-4' />}
+    >
+        <div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'>
+          <div className='space-y-1'>
+            <div className='text-sm font-medium'>{t('Interface Language')}</div>
+            <p className='text-muted-foreground line-clamp-2 text-xs sm:text-sm'>
+              {t(
+                'Language preferences sync across your signed-in devices and affect API error messages.'
+              )}
+            </p>
+          </div>
+          <div className='flex items-center gap-2 sm:min-w-48'>
+            <Select
+              value={currentLanguage}
+              onValueChange={handleLanguageChange}
+              disabled={saving}
+            >
+              <SelectTrigger className='w-full sm:w-48'>
+                <SelectValue placeholder={t('Select language')} />
+              </SelectTrigger>
+              <SelectContent>
+                {LANGUAGE_OPTIONS.map((language) => (
+                  <SelectItem key={language.value} value={language.value}>
+                    {language.label}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            {saving && (
+              <Loader2 className='text-muted-foreground size-4 animate-spin' />
+            )}
+          </div>
+        </div>
+    </TitledCard>
+  )
+}

+ 86 - 74
web/default/src/features/profile/components/passkey-card.tsx

@@ -1,5 +1,5 @@
 import { useCallback, useMemo, useState } from 'react'
 import { useCallback, useMemo, useState } from 'react'
-import { KeyRound, ShieldAlert, Loader2 } from 'lucide-react'
+import { AlertTriangle, KeyRound, Loader2, ShieldAlert } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { toast } from 'sonner'
 import { toast } from 'sonner'
 import dayjs from '@/lib/dayjs'
 import dayjs from '@/lib/dayjs'
@@ -169,14 +169,13 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
 
 
   if (pageLoading || loading) {
   if (pageLoading || loading) {
     return (
     return (
-      <Card className='overflow-hidden'>
-        <CardHeader>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='p-3 sm:p-5'>
           <Skeleton className='h-6 w-48' />
           <Skeleton className='h-6 w-48' />
           <Skeleton className='mt-2 h-4 w-64' />
           <Skeleton className='mt-2 h-4 w-64' />
         </CardHeader>
         </CardHeader>
-        <CardContent className='space-y-4'>
-          <Skeleton className='h-12 w-full' />
-          <Skeleton className='h-12 w-full' />
+        <CardContent className='p-3 sm:p-5'>
+          <Skeleton className='h-20 w-full' />
         </CardContent>
         </CardContent>
       </Card>
       </Card>
     )
     )
@@ -191,19 +190,20 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
 
 
   return (
   return (
     <>
     <>
-      <Card className='overflow-hidden'>
-        <CardHeader>
-          <CardTitle className='text-xl tracking-tight'>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='p-3 sm:p-5'>
+          <CardTitle className='text-lg tracking-tight sm:text-xl'>
             {t('Passkey Login')}
             {t('Passkey Login')}
           </CardTitle>
           </CardTitle>
-          <CardDescription>
+          <CardDescription className='text-xs sm:text-sm'>
             {t('Use Passkey to sign in without entering your password.')}
             {t('Use Passkey to sign in without entering your password.')}
           </CardDescription>
           </CardDescription>
         </CardHeader>
         </CardHeader>
 
 
-        <CardContent className='space-y-6'>
-          <div className='flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between xl:flex-col xl:items-stretch 2xl:flex-row 2xl:items-center'>
-            <div className='flex items-start gap-3'>
+        <CardContent className='p-3 sm:p-5'>
+          <div className='space-y-6'>
+            <div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between xl:flex-col 2xl:flex-row'>
+              <div className='flex items-start gap-4'>
               <div className='bg-muted rounded-md p-2'>
               <div className='bg-muted rounded-md p-2'>
                 <KeyRound className='h-5 w-5' />
                 <KeyRound className='h-5 w-5' />
               </div>
               </div>
@@ -241,74 +241,86 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
                   {t('Last used:')} {formattedLastUsed}
                   {t('Last used:')} {formattedLastUsed}
                 </p>
                 </p>
               </div>
               </div>
+              </div>
+
+              {!enabled && (
+                <Button
+                  className='w-full sm:w-auto xl:w-full 2xl:w-auto'
+                  onClick={handleRegister}
+                  disabled={!supported || registering}
+                >
+                  {registering && (
+                    <Loader2 className='mr-2 h-4 w-4 animate-spin' />
+                  )}
+                  {t('Enable Passkey')}
+                </Button>
+              )}
             </div>
             </div>
 
 
-            {!enabled ? (
-              <Button
-                className='w-full sm:w-auto xl:w-full 2xl:w-auto'
-                onClick={handleRegister}
-                disabled={!supported || registering}
-              >
-                {registering && (
-                  <Loader2 className='mr-2 h-4 w-4 animate-spin' />
-                )}
-                {t('Register Passkey')}
-              </Button>
-            ) : (
-              <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
-                <AlertDialogTrigger asChild>
-                  <Button
-                    variant='outline'
-                    className='w-full sm:w-auto xl:w-full 2xl:w-auto'
-                    disabled={removing}
-                  >
-                    {t('Remove Passkey')}
-                  </Button>
-                </AlertDialogTrigger>
-                <AlertDialogContent>
-                  <AlertDialogHeader>
-                    <AlertDialogTitle>{t('Remove Passkey?')}</AlertDialogTitle>
-                    <AlertDialogDescription>
-                      {t(
-                        'Removing Passkey will require you to sign in with your password next time. You can re-register anytime.'
-                      )}
-                    </AlertDialogDescription>
-                  </AlertDialogHeader>
-                  <AlertDialogFooter>
-                    <AlertDialogCancel disabled={removing}>
-                      {t('Cancel')}
-                    </AlertDialogCancel>
-                    <AlertDialogAction
-                      className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
+            {enabled && (
+              <div className='flex flex-col gap-3 border-t pt-6 sm:flex-row xl:flex-col 2xl:flex-row'>
+                <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
+                  <AlertDialogTrigger asChild>
+                    <Button
+                      variant='destructive'
+                      className='flex-1'
                       disabled={removing}
                       disabled={removing}
-                      onClick={(event) => {
-                        event.preventDefault()
-                        handleRemove()
-                      }}
                     >
                     >
-                      {t('Remove')}
-                    </AlertDialogAction>
-                  </AlertDialogFooter>
-                </AlertDialogContent>
-              </AlertDialog>
+                      {removing ? (
+                        <Loader2 className='mr-2 h-4 w-4 animate-spin' />
+                      ) : (
+                        <AlertTriangle className='mr-2 h-4 w-4' />
+                      )}
+                      {t('Remove Passkey')}
+                    </Button>
+                  </AlertDialogTrigger>
+                  <AlertDialogContent>
+                    <AlertDialogHeader>
+                      <AlertDialogTitle>
+                        {t('Remove Passkey?')}
+                      </AlertDialogTitle>
+                      <AlertDialogDescription>
+                        {t(
+                          'Removing Passkey will require you to sign in with your password next time. You can re-register anytime.'
+                        )}
+                      </AlertDialogDescription>
+                    </AlertDialogHeader>
+                    <AlertDialogFooter>
+                      <AlertDialogCancel disabled={removing}>
+                        {t('Cancel')}
+                      </AlertDialogCancel>
+                      <AlertDialogAction
+                        className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
+                        disabled={removing}
+                        onClick={(event) => {
+                          event.preventDefault()
+                          handleRemove()
+                        }}
+                      >
+                        {t('Remove')}
+                      </AlertDialogAction>
+                    </AlertDialogFooter>
+                  </AlertDialogContent>
+                </AlertDialog>
+              </div>
             )}
             )}
-          </div>
 
 
-          {showUnsupportedNotice && (
-            <div className='bg-muted/60 text-muted-foreground flex items-start gap-3 rounded-md p-4 text-sm'>
-              <ShieldAlert className='mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500' />
-              <div>
-                <p className='text-foreground font-medium'>
-                  {t('Passkey not supported on this device')}
-                </p>
-                <p>
-                  {t(
-                    'Use a compatible browser or device with biometric authentication or a security key to register a Passkey.'
-                  )}
-                </p>
+            {showUnsupportedNotice && (
+              <div className='bg-muted/60 text-muted-foreground flex items-start gap-3 rounded-md p-4 text-sm'>
+                <ShieldAlert className='mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500' />
+                <div>
+                  <p className='text-foreground font-medium'>
+                    {t('Passkey not supported on this device')}
+                  </p>
+                  <p>
+                    {t(
+                      'Use a compatible browser or device with biometric authentication or a security key to register a Passkey.'
+                    )}
+                  </p>
+                </div>
               </div>
               </div>
-            </div>
-          )}
+            )}
+          </div>
         </CardContent>
         </CardContent>
       </Card>
       </Card>
 
 

+ 16 - 16
web/default/src/features/profile/components/profile-header.tsx

@@ -82,17 +82,17 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
 
 
   return (
   return (
     <div className='bg-card overflow-hidden rounded-lg border'>
     <div className='bg-card overflow-hidden rounded-lg border'>
-      <div className='p-4 sm:p-5'>
-        <div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
-          <Avatar className='ring-background h-16 w-16 rounded-2xl text-lg ring-4'>
-            <AvatarFallback className='bg-primary/10 text-primary rounded-2xl'>
+      <div className='p-3 sm:p-5'>
+        <div className='flex items-center gap-3 text-left sm:gap-4'>
+          <Avatar className='ring-background h-12 w-12 rounded-xl text-sm ring-2 sm:h-16 sm:w-16 sm:rounded-2xl sm:text-lg sm:ring-4'>
+            <AvatarFallback className='bg-primary/10 text-primary rounded-xl sm:rounded-2xl'>
               {initials}
               {initials}
             </AvatarFallback>
             </AvatarFallback>
           </Avatar>
           </Avatar>
 
 
-          <div className='min-w-0 flex-1 space-y-3'>
-            <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-start'>
-              <h1 className='text-2xl font-semibold tracking-tight'>
+          <div className='min-w-0 flex-1 space-y-1.5 sm:space-y-3'>
+            <div className='flex min-w-0 items-center gap-2'>
+              <h1 className='truncate text-xl font-semibold tracking-tight sm:text-2xl'>
                 {displayName}
                 {displayName}
               </h1>
               </h1>
               <StatusBadge
               <StatusBadge
@@ -102,18 +102,18 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
               />
               />
             </div>
             </div>
 
 
-            <div className='text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-start sm:gap-4'>
-              <span>@{profile.username}</span>
+            <div className='text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs sm:gap-x-4 sm:text-sm'>
+              <span className='truncate'>@{profile.username}</span>
               {profile.email && (
               {profile.email && (
                 <>
                 <>
-                  <span className='hidden sm:inline'>•</span>
-                  <span>{profile.email}</span>
+                  <span>•</span>
+                  <span className='truncate'>{profile.email}</span>
                 </>
                 </>
               )}
               )}
               {profile.group && (
               {profile.group && (
                 <>
                 <>
-                  <span className='hidden sm:inline'>•</span>
-                  <span>{profile.group}</span>
+                  <span>•</span>
+                  <span className='truncate'>{profile.group}</span>
                 </>
                 </>
               )}
               )}
             </div>
             </div>
@@ -121,9 +121,9 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
         </div>
         </div>
       </div>
       </div>
       <div className='border-t'>
       <div className='border-t'>
-        <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
+        <div className='divide-border/60 grid grid-cols-3 divide-x'>
           {stats.map((item) => (
           {stats.map((item) => (
-            <div key={item.label} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+            <div key={item.label} className='min-w-0 px-3 py-3 sm:px-5 sm:py-4'>
               <div className='flex items-center gap-2'>
               <div className='flex items-center gap-2'>
                 <item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
                 <item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
                 <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
                 <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
@@ -131,7 +131,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
                 </div>
                 </div>
               </div>
               </div>
 
 
-              <div className='text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'>
+              <div className='text-foreground mt-1.5 truncate font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
                 {item.value}
                 {item.value}
               </div>
               </div>
               <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
               <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>

+ 18 - 31
web/default/src/features/profile/components/profile-security-card.tsx

@@ -4,11 +4,10 @@ import { useDialogs } from '@/hooks/use-dialog'
 import {
 import {
   Card,
   Card,
   CardContent,
   CardContent,
-  CardDescription,
   CardHeader,
   CardHeader,
-  CardTitle,
 } from '@/components/ui/card'
 } from '@/components/ui/card'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Skeleton } from '@/components/ui/skeleton'
+import { TitledCard } from '@/components/ui/titled-card'
 import type { UserProfile } from '../types'
 import type { UserProfile } from '../types'
 import { AccessTokenDialog } from './dialogs/access-token-dialog'
 import { AccessTokenDialog } from './dialogs/access-token-dialog'
 import { ChangePasswordDialog } from './dialogs/change-password-dialog'
 import { ChangePasswordDialog } from './dialogs/change-password-dialog'
@@ -34,12 +33,12 @@ export function ProfileSecurityCard({
 
 
   if (loading) {
   if (loading) {
     return (
     return (
-      <Card className='overflow-hidden'>
-        <CardHeader className='border-b'>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='h-6 w-32' />
           <Skeleton className='mt-2 h-4 w-48' />
           <Skeleton className='mt-2 h-4 w-48' />
         </CardHeader>
         </CardHeader>
-        <CardContent className='space-y-3 pt-6'>
+        <CardContent className='space-y-3 p-3 sm:p-5'>
           {Array.from({ length: 3 }).map((_, i) => (
           {Array.from({ length: 3 }).map((_, i) => (
             <Skeleton key={i} className='h-16 w-full' />
             <Skeleton key={i} className='h-16 w-full' />
           ))}
           ))}
@@ -76,31 +75,18 @@ export function ProfileSecurityCard({
 
 
   return (
   return (
     <>
     <>
-      <Card className='overflow-hidden'>
-        <CardHeader className='border-b'>
-          <div className='flex items-center gap-3'>
-            <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
-              <Shield className='h-4 w-4' />
-            </div>
-            <div className='min-w-0'>
-              <CardTitle className='text-xl tracking-tight'>
-                {t('Security')}
-              </CardTitle>
-              <CardDescription>
-                {t('Manage your security settings and account access')}
-              </CardDescription>
-            </div>
-          </div>
-        </CardHeader>
-
-        <CardContent className='pt-6'>
-          <div className='grid grid-cols-1 gap-3 md:grid-cols-3'>
+      <TitledCard
+        title={t('Security')}
+        description={t('Manage your security settings and account access')}
+        icon={<Shield className='h-4 w-4' />}
+      >
+          <div className='grid grid-cols-1 gap-2.5 sm:gap-3 md:grid-cols-3'>
             {securityActions.map((item) => (
             {securityActions.map((item) => (
               <button
               <button
                 key={item.title}
                 key={item.title}
                 type='button'
                 type='button'
                 onClick={item.action}
                 onClick={item.action}
-                className={`hover:bg-muted/50 flex flex-col items-center gap-2 rounded-lg border p-4 text-center transition-colors ${
+                className={`hover:bg-muted/50 flex items-center gap-3 rounded-lg border p-3 text-left transition-colors md:flex-col md:gap-2 md:p-4 md:text-center ${
                   item.variant === 'destructive'
                   item.variant === 'destructive'
                     ? 'border-destructive/30 hover:border-destructive/50 hover:bg-destructive/5'
                     ? 'border-destructive/30 hover:border-destructive/50 hover:bg-destructive/5'
                     : ''
                     : ''
@@ -115,15 +101,16 @@ export function ProfileSecurityCard({
                 >
                 >
                   <item.icon className='h-5 w-5' />
                   <item.icon className='h-5 w-5' />
                 </div>
                 </div>
-                <p className='text-sm font-medium'>{item.title}</p>
-                <p className='text-muted-foreground text-xs'>
-                  {item.description}
-                </p>
+                <div className='min-w-0 md:contents'>
+                  <p className='text-sm font-medium'>{item.title}</p>
+                  <p className='text-muted-foreground line-clamp-1 text-xs md:line-clamp-none'>
+                    {item.description}
+                  </p>
+                </div>
               </button>
               </button>
             ))}
             ))}
           </div>
           </div>
-        </CardContent>
-      </Card>
+      </TitledCard>
 
 
       {/* Dialogs */}
       {/* Dialogs */}
       <ChangePasswordDialog
       <ChangePasswordDialog

+ 14 - 29
web/default/src/features/profile/components/profile-settings-card.tsx

@@ -4,12 +4,11 @@ import { useTranslation } from 'react-i18next'
 import {
 import {
   Card,
   Card,
   CardContent,
   CardContent,
-  CardDescription,
   CardHeader,
   CardHeader,
-  CardTitle,
 } from '@/components/ui/card'
 } from '@/components/ui/card'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { TitledCard } from '@/components/ui/titled-card'
 import type { UserProfile } from '../types'
 import type { UserProfile } from '../types'
 import { AccountBindingsTab } from './tabs/account-bindings-tab'
 import { AccountBindingsTab } from './tabs/account-bindings-tab'
 import { NotificationTab } from './tabs/notification-tab'
 import { NotificationTab } from './tabs/notification-tab'
@@ -34,12 +33,12 @@ export function ProfileSettingsCard({
 
 
   if (loading) {
   if (loading) {
     return (
     return (
-      <Card className='overflow-hidden'>
-        <CardHeader className='border-b'>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='h-6 w-32' />
           <Skeleton className='mt-2 h-4 w-48' />
           <Skeleton className='mt-2 h-4 w-48' />
         </CardHeader>
         </CardHeader>
-        <CardContent className='space-y-4 pt-6'>
+        <CardContent className='space-y-4 p-3 sm:p-5'>
           <Skeleton className='h-10 w-full' />
           <Skeleton className='h-10 w-full' />
           {Array.from({ length: 3 }).map((_, i) => (
           {Array.from({ length: 3 }).map((_, i) => (
             <Skeleton key={i} className='h-20 w-full' />
             <Skeleton key={i} className='h-20 w-full' />
@@ -50,29 +49,16 @@ export function ProfileSettingsCard({
   }
   }
 
 
   return (
   return (
-    <Card className='overflow-hidden'>
-      <CardHeader className='border-b'>
-        <div className='flex items-center gap-3'>
-          <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
-            <Settings className='h-4 w-4' />
-          </div>
-          <div className='min-w-0'>
-            <CardTitle className='text-xl tracking-tight'>
-              {t('Settings')}
-            </CardTitle>
-            <CardDescription>
-              {t('Configure your account preferences and integrations')}
-            </CardDescription>
-          </div>
-        </div>
-      </CardHeader>
-
-      <CardContent className='pt-6'>
+    <TitledCard
+      title={t('Settings')}
+      description={t('Configure your account preferences and integrations')}
+      icon={<Settings className='h-4 w-4' />}
+    >
         <Tabs value={activeTab} onValueChange={setActiveTab}>
         <Tabs value={activeTab} onValueChange={setActiveTab}>
           <TabsList className='grid h-auto w-full grid-cols-2 gap-1 rounded-xl p-1'>
           <TabsList className='grid h-auto w-full grid-cols-2 gap-1 rounded-xl p-1'>
             <TabsTrigger
             <TabsTrigger
               value='bindings'
               value='bindings'
-              className='h-auto gap-2 rounded-lg px-3 py-2.5'
+              className='h-auto gap-2 rounded-lg px-3 py-2'
             >
             >
               <Link2 className='h-4 w-4' />
               <Link2 className='h-4 w-4' />
               <span className='hidden sm:inline'>{t('Account Bindings')}</span>
               <span className='hidden sm:inline'>{t('Account Bindings')}</span>
@@ -80,7 +66,7 @@ export function ProfileSettingsCard({
             </TabsTrigger>
             </TabsTrigger>
             <TabsTrigger
             <TabsTrigger
               value='settings'
               value='settings'
-              className='h-auto gap-2 rounded-lg px-3 py-2.5'
+              className='h-auto gap-2 rounded-lg px-3 py-2'
             >
             >
               <Settings className='h-4 w-4' />
               <Settings className='h-4 w-4' />
               <span className='hidden sm:inline'>
               <span className='hidden sm:inline'>
@@ -90,15 +76,14 @@ export function ProfileSettingsCard({
             </TabsTrigger>
             </TabsTrigger>
           </TabsList>
           </TabsList>
 
 
-          <TabsContent value='bindings' className='mt-6'>
+          <TabsContent value='bindings' className='mt-4 sm:mt-6'>
             <AccountBindingsTab profile={profile} onUpdate={onProfileUpdate} />
             <AccountBindingsTab profile={profile} onUpdate={onProfileUpdate} />
           </TabsContent>
           </TabsContent>
 
 
-          <TabsContent value='settings' className='mt-6'>
+          <TabsContent value='settings' className='mt-4 sm:mt-6'>
             <NotificationTab profile={profile} onUpdate={onProfileUpdate} />
             <NotificationTab profile={profile} onUpdate={onProfileUpdate} />
           </TabsContent>
           </TabsContent>
         </Tabs>
         </Tabs>
-      </CardContent>
-    </Card>
+    </TitledCard>
   )
   )
 }
 }

+ 6 - 6
web/default/src/features/profile/components/sidebar-modules-card.tsx

@@ -182,23 +182,23 @@ export function SidebarModulesCard() {
   }
   }
 
 
   return (
   return (
-    <Card className='overflow-hidden'>
-      <CardHeader className='border-b'>
+    <Card className='gap-0 overflow-hidden py-0'>
+      <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
         <div className='flex items-center gap-3'>
         <div className='flex items-center gap-3'>
-          <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
+          <div className='bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg sm:h-9 sm:w-9'>
             <LayoutDashboard className='h-4 w-4' />
             <LayoutDashboard className='h-4 w-4' />
           </div>
           </div>
           <div className='min-w-0'>
           <div className='min-w-0'>
-            <CardTitle className='text-xl tracking-tight'>
+            <CardTitle className='text-lg tracking-tight sm:text-xl'>
               {t('Sidebar Personal Settings')}
               {t('Sidebar Personal Settings')}
             </CardTitle>
             </CardTitle>
-            <CardDescription>
+            <CardDescription className='text-xs sm:text-sm'>
               {t('Customize sidebar display content')}
               {t('Customize sidebar display content')}
             </CardDescription>
             </CardDescription>
           </div>
           </div>
         </div>
         </div>
       </CardHeader>
       </CardHeader>
-      <CardContent className='space-y-5 pt-6'>
+      <CardContent className='space-y-4 p-3 sm:space-y-5 sm:p-5'>
         {sectionDefs.map((section) => {
         {sectionDefs.map((section) => {
           const sectionEnabled = config[section.key]?.enabled !== false
           const sectionEnabled = config[section.key]?.enabled !== false
           return (
           return (

+ 11 - 11
web/default/src/features/profile/components/tabs/account-bindings-tab.tsx

@@ -245,14 +245,14 @@ export function AccountBindingsTab({
 
 
   return (
   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) => (
         {bindings.map((binding) => (
           <div
           <div
             key={binding.id}
             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' />
                 <binding.icon className='h-4 w-4' />
               </div>
               </div>
               <div className='min-w-0'>
               <div className='min-w-0'>
@@ -274,7 +274,7 @@ export function AccountBindingsTab({
             <Button
             <Button
               variant='outline'
               variant='outline'
               size='sm'
               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}
               onClick={binding.onBind}
               disabled={binding.isBound && binding.id !== 'email'}
               disabled={binding.isBound && binding.id !== 'email'}
             >
             >
@@ -295,7 +295,7 @@ export function AccountBindingsTab({
           <p className='text-muted-foreground mb-3 text-sm font-medium'>
           <p className='text-muted-foreground mb-3 text-sm font-medium'>
             {t('Custom OAuth')}
             {t('Custom OAuth')}
           </p>
           </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) => {
             {customProviders.map((provider) => {
               const binding = customBindings.find(
               const binding = customBindings.find(
                 (b) => b.provider_id === provider.id
                 (b) => b.provider_id === provider.id
@@ -304,10 +304,10 @@ export function AccountBindingsTab({
               return (
               return (
                 <div
                 <div
                   key={provider.id}
                   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' />
                       <Link2 className='h-4 w-4' />
                     </div>
                     </div>
                     <div className='min-w-0'>
                     <div className='min-w-0'>
@@ -332,7 +332,7 @@ export function AccountBindingsTab({
                     <Button
                     <Button
                       variant='ghost'
                       variant='ghost'
                       size='sm'
                       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)}
                       onClick={() => setUnbindTarget(binding)}
                     >
                     >
                       <Unlink className='mr-1 h-3 w-3' />
                       <Unlink className='mr-1 h-3 w-3' />
@@ -342,7 +342,7 @@ export function AccountBindingsTab({
                     <Button
                     <Button
                       variant='outline'
                       variant='outline'
                       size='sm'
                       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)}
                       onClick={() => handleBindCustomOAuth(provider)}
                     >
                     >
                       {t('Bind')}
                       {t('Bind')}

+ 31 - 23
web/default/src/features/profile/components/tabs/notification-tab.tsx

@@ -102,16 +102,16 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
   }
   }
 
 
   return (
   return (
-    <div className='space-y-6'>
+    <div className='space-y-4 sm:space-y-6'>
       {/* Notification Type */}
       {/* Notification Type */}
-      <div className='space-y-3'>
+      <div className='space-y-2.5'>
         <Label>{t('Notification Method')}</Label>
         <Label>{t('Notification Method')}</Label>
         <RadioGroup
         <RadioGroup
           value={settings.notify_type}
           value={settings.notify_type}
           onValueChange={(value) =>
           onValueChange={(value) =>
             updateField('notify_type', value as NotifyType)
             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) => {
           {NOTIFICATION_METHODS.map((method) => {
             const Icon = NOTIFICATION_ICONS[method.value]
             const Icon = NOTIFICATION_ICONS[method.value]
@@ -120,7 +120,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
               <Label
               <Label
                 key={method.value}
                 key={method.value}
                 htmlFor={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
                   isSelected
                     ? 'border-primary bg-primary/5 text-primary'
                     ? 'border-primary bg-primary/5 text-primary'
                     : 'border-muted hover:border-muted-foreground/25 hover:bg-muted/50'
                     : 'border-muted hover:border-muted-foreground/25 hover:bg-muted/50'
@@ -131,8 +131,10 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
                   id={method.value}
                   id={method.value}
                   className='sr-only'
                   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>
               </Label>
             )
             )
           })}
           })}
@@ -140,11 +142,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
       </div>
       </div>
 
 
       {/* Warning Threshold */}
       {/* Warning Threshold */}
-      <div className='space-y-2'>
+      <div className='space-y-1.5'>
         <Label htmlFor='threshold'>{t('Quota Warning Threshold')}</Label>
         <Label htmlFor='threshold'>{t('Quota Warning Threshold')}</Label>
         <Input
         <Input
           id='threshold'
           id='threshold'
           type='number'
           type='number'
+          className='h-9'
           value={settings.quota_warning_threshold}
           value={settings.quota_warning_threshold}
           onChange={(e) =>
           onChange={(e) =>
             updateField('quota_warning_threshold', Number(e.target.value))
             updateField('quota_warning_threshold', Number(e.target.value))
@@ -158,11 +161,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
 
 
       {/* Email Settings */}
       {/* Email Settings */}
       {settings.notify_type === 'email' && (
       {settings.notify_type === 'email' && (
-        <div className='space-y-2'>
+        <div className='space-y-1.5'>
           <Label htmlFor='notifyEmail'>{t('Notification Email')}</Label>
           <Label htmlFor='notifyEmail'>{t('Notification Email')}</Label>
           <Input
           <Input
             id='notifyEmail'
             id='notifyEmail'
             type='email'
             type='email'
+            className='h-9'
             value={settings.notification_email}
             value={settings.notification_email}
             onChange={(e) => updateField('notification_email', e.target.value)}
             onChange={(e) => updateField('notification_email', e.target.value)}
             placeholder={t('Leave empty to use account email')}
             placeholder={t('Leave empty to use account email')}
@@ -173,17 +177,18 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
       {/* Webhook Settings */}
       {/* Webhook Settings */}
       {settings.notify_type === 'webhook' && (
       {settings.notify_type === 'webhook' && (
         <>
         <>
-          <div className='space-y-2'>
+          <div className='space-y-1.5'>
             <Label htmlFor='webhookUrl'>{t('Webhook URL')}</Label>
             <Label htmlFor='webhookUrl'>{t('Webhook URL')}</Label>
             <Input
             <Input
               id='webhookUrl'
               id='webhookUrl'
               type='url'
               type='url'
+              className='h-9'
               value={settings.webhook_url}
               value={settings.webhook_url}
               onChange={(e) => updateField('webhook_url', e.target.value)}
               onChange={(e) => updateField('webhook_url', e.target.value)}
               placeholder={t('https://example.com/webhook')}
               placeholder={t('https://example.com/webhook')}
             />
             />
           </div>
           </div>
-          <div className='space-y-2'>
+          <div className='space-y-1.5'>
             <Label htmlFor='webhookSecret'>{t('Webhook Secret')}</Label>
             <Label htmlFor='webhookSecret'>{t('Webhook Secret')}</Label>
             <PasswordInput
             <PasswordInput
               id='webhookSecret'
               id='webhookSecret'
@@ -197,11 +202,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
 
 
       {/* Bark Settings */}
       {/* Bark Settings */}
       {settings.notify_type === 'bark' && (
       {settings.notify_type === 'bark' && (
-        <div className='space-y-2'>
+        <div className='space-y-1.5'>
           <Label htmlFor='barkUrl'>{t('Bark Push URL')}</Label>
           <Label htmlFor='barkUrl'>{t('Bark Push URL')}</Label>
           <Input
           <Input
             id='barkUrl'
             id='barkUrl'
             type='url'
             type='url'
+            className='h-9'
             value={settings.bark_url}
             value={settings.bark_url}
             onChange={(e) => updateField('bark_url', e.target.value)}
             onChange={(e) => updateField('bark_url', e.target.value)}
             placeholder={t('https://api.day.app/yourkey/{{title}}/{{content}}')}
             placeholder={t('https://api.day.app/yourkey/{{title}}/{{content}}')}
@@ -215,11 +221,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
       {/* Gotify Settings */}
       {/* Gotify Settings */}
       {settings.notify_type === 'gotify' && (
       {settings.notify_type === 'gotify' && (
         <>
         <>
-          <div className='space-y-2'>
+          <div className='space-y-1.5'>
             <Label htmlFor='gotifyUrl'>{t('Gotify Server URL')}</Label>
             <Label htmlFor='gotifyUrl'>{t('Gotify Server URL')}</Label>
             <Input
             <Input
               id='gotifyUrl'
               id='gotifyUrl'
               type='url'
               type='url'
+              className='h-9'
               value={settings.gotify_url}
               value={settings.gotify_url}
               onChange={(e) => updateField('gotify_url', e.target.value)}
               onChange={(e) => updateField('gotify_url', e.target.value)}
               placeholder={t('https://gotify.example.com')}
               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')}
               {t('Enter the full URL of your Gotify server')}
             </p>
             </p>
           </div>
           </div>
-          <div className='space-y-2'>
+          <div className='space-y-1.5'>
             <Label htmlFor='gotifyToken'>{t('Gotify Application Token')}</Label>
             <Label htmlFor='gotifyToken'>{t('Gotify Application Token')}</Label>
             <PasswordInput
             <PasswordInput
               id='gotifyToken'
               id='gotifyToken'
@@ -240,11 +247,12 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
               {t('Token obtained from your Gotify application')}
               {t('Token obtained from your Gotify application')}
             </p>
             </p>
           </div>
           </div>
-          <div className='space-y-2'>
+          <div className='space-y-1.5'>
             <Label htmlFor='gotifyPriority'>{t('Message Priority')}</Label>
             <Label htmlFor='gotifyPriority'>{t('Message Priority')}</Label>
             <Input
             <Input
               id='gotifyPriority'
               id='gotifyPriority'
               type='number'
               type='number'
+              className='h-9'
               min='0'
               min='0'
               max='10'
               max='10'
               value={settings.gotify_priority}
               value={settings.gotify_priority}
@@ -259,8 +267,8 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
               )}
               )}
             </p>
             </p>
           </div>
           </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')}
               {t('Setup Instructions')}
             </h5>
             </h5>
             <ol className='text-muted-foreground space-y-1 text-xs'>
             <ol className='text-muted-foreground space-y-1 text-xs'>
@@ -287,7 +295,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
       <div className='border-t' />
       <div className='border-t' />
 
 
       {/* Preferences Section */}
       {/* Preferences Section */}
-      <div className='space-y-4'>
+      <div className='space-y-3'>
         <div>
         <div>
           <h4 className='text-sm font-medium'>{t('Preferences')}</h4>
           <h4 className='text-sm font-medium'>{t('Preferences')}</h4>
           <p className='text-muted-foreground mt-1 text-xs'>
           <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) */}
         {/* Receive Upstream Model Update Notifications (admin only) */}
         {isAdmin && (
         {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'>
             <div className='space-y-0.5'>
               <Label htmlFor='upstreamModelUpdateNotify'>
               <Label htmlFor='upstreamModelUpdateNotify'>
                 {t('Receive Upstream Model Update Notifications')}
                 {t('Receive Upstream Model Update Notifications')}
               </Label>
               </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(
                 {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.'
                   '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 */}
         {/* 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'>
           <div className='space-y-0.5'>
             <Label htmlFor='acceptUnsetPrice'>
             <Label htmlFor='acceptUnsetPrice'>
               {t('Accept Unpriced Models')}
               {t('Accept Unpriced Models')}
             </Label>
             </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')}
               {t('Allow using models without price configuration')}
             </p>
             </p>
           </div>
           </div>
@@ -340,10 +348,10 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
         </div>
         </div>
 
 
         {/* Record IP Log */}
         {/* 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'>
           <div className='space-y-0.5'>
             <Label htmlFor='recordIp'>{t('Record IP Address')}</Label>
             <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')}
               {t('Log IP address for usage and error logs')}
             </p>
             </p>
           </div>
           </div>

+ 8 - 8
web/default/src/features/profile/components/two-fa-card.tsx

@@ -33,12 +33,12 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
 
 
   if (pageLoading || loading) {
   if (pageLoading || loading) {
     return (
     return (
-      <Card className='overflow-hidden'>
-        <CardHeader>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='p-3 sm:p-5'>
           <Skeleton className='h-6 w-48' />
           <Skeleton className='h-6 w-48' />
           <Skeleton className='mt-2 h-4 w-64' />
           <Skeleton className='mt-2 h-4 w-64' />
         </CardHeader>
         </CardHeader>
-        <CardContent>
+        <CardContent className='p-3 sm:p-5'>
           <Skeleton className='h-20 w-full' />
           <Skeleton className='h-20 w-full' />
         </CardContent>
         </CardContent>
       </Card>
       </Card>
@@ -47,17 +47,17 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
 
 
   return (
   return (
     <>
     <>
-      <Card className='overflow-hidden'>
-        <CardHeader>
-          <CardTitle className='text-xl tracking-tight'>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='p-3 sm:p-5'>
+          <CardTitle className='text-lg tracking-tight sm:text-xl'>
             {t('Two-Factor Authentication')}
             {t('Two-Factor Authentication')}
           </CardTitle>
           </CardTitle>
-          <CardDescription>
+          <CardDescription className='text-xs sm:text-sm'>
             {t('Add an extra layer of security to your account')}
             {t('Add an extra layer of security to your account')}
           </CardDescription>
           </CardDescription>
         </CardHeader>
         </CardHeader>
 
 
-        <CardContent>
+        <CardContent className='p-3 sm:p-5'>
           <div className='space-y-6'>
           <div className='space-y-6'>
             {/* Status Section */}
             {/* Status Section */}
             <div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between xl:flex-col 2xl:flex-row'>
             <div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between xl:flex-col 2xl:flex-row'>

+ 10 - 5
web/default/src/features/profile/index.tsx

@@ -6,6 +6,7 @@ import {
   CardStaggerItem,
   CardStaggerItem,
 } from '@/components/page-transition'
 } from '@/components/page-transition'
 import { CheckinCalendarCard } from './components/checkin-calendar-card'
 import { CheckinCalendarCard } from './components/checkin-calendar-card'
+import { LanguagePreferencesCard } from './components/language-preferences-card'
 import { PasskeyCard } from './components/passkey-card'
 import { PasskeyCard } from './components/passkey-card'
 import { ProfileHeader } from './components/profile-header'
 import { ProfileHeader } from './components/profile-header'
 import { ProfileSecurityCard } from './components/profile-security-card'
 import { ProfileSecurityCard } from './components/profile-security-card'
@@ -30,24 +31,28 @@ export function Profile() {
     <>
     <>
       <AppHeader />
       <AppHeader />
       <Main>
       <Main>
-        <div className='min-h-0 flex-1 overflow-auto px-4 py-4 sm:py-6'>
-          <CardStaggerContainer className='mx-auto flex w-full max-w-7xl flex-col gap-5 sm:gap-6'>
+        <div className='min-h-0 flex-1 overflow-auto px-3 py-3 sm:px-4 sm:py-6'>
+          <CardStaggerContainer className='mx-auto flex w-full max-w-7xl flex-col gap-4 sm:gap-6'>
             <CardStaggerItem>
             <CardStaggerItem>
               <ProfileHeader profile={profile} loading={loading} />
               <ProfileHeader profile={profile} loading={loading} />
             </CardStaggerItem>
             </CardStaggerItem>
 
 
             <CardStaggerItem>
             <CardStaggerItem>
-              <div className='grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.46fr)] xl:items-start'>
-                <div className='space-y-5 sm:space-y-6'>
+              <div className='grid gap-4 sm:gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.46fr)] xl:items-start'>
+                <div className='space-y-4 sm:space-y-6'>
                   <ProfileSettingsCard
                   <ProfileSettingsCard
                     profile={profile}
                     profile={profile}
                     loading={loading}
                     loading={loading}
                     onProfileUpdate={refreshProfile}
                     onProfileUpdate={refreshProfile}
                   />
                   />
+                  <LanguagePreferencesCard
+                    profile={profile}
+                    onProfileUpdate={refreshProfile}
+                  />
                   <ProfileSecurityCard profile={profile} loading={loading} />
                   <ProfileSecurityCard profile={profile} loading={loading} />
                 </div>
                 </div>
 
 
-                <div className='space-y-5 sm:space-y-6 xl:sticky xl:top-6'>
+                <div className='space-y-4 sm:space-y-6 xl:sticky xl:top-6'>
                   {checkinEnabled && (
                   {checkinEnabled && (
                     <CheckinCalendarCard
                     <CheckinCalendarCard
                       checkinEnabled={checkinEnabled}
                       checkinEnabled={checkinEnabled}

+ 2 - 0
web/default/src/features/profile/types.ts

@@ -98,6 +98,8 @@ export interface UserSettings {
   record_ip_log?: boolean
   record_ip_log?: boolean
   /** Receive upstream model update notifications (admin only) */
   /** Receive upstream model update notifications (admin only) */
   upstream_model_update_notify_enabled?: boolean
   upstream_model_update_notify_enabled?: boolean
+  /** Preferred interface/API response language */
+  language?: string
 }
 }
 
 
 /**
 /**

+ 5 - 5
web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx

@@ -133,8 +133,8 @@ export function RedemptionsMutateDrawer({
         }
         }
       }}
       }}
     >
     >
-      <SheetContent className='flex w-full flex-col sm:max-w-[600px]'>
-        <SheetHeader className='text-start'>
+      <SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'>
+        <SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
           <SheetTitle>
           <SheetTitle>
             {isUpdate
             {isUpdate
               ? t('Update Redemption Code')
               ? t('Update Redemption Code')
@@ -153,7 +153,7 @@ export function RedemptionsMutateDrawer({
           <form
           <form
             id='redemption-form'
             id='redemption-form'
             onSubmit={form.handleSubmit(onSubmit)}
             onSubmit={form.handleSubmit(onSubmit)}
-            className='flex-1 space-y-6 overflow-y-auto px-4'
+            className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4'
           >
           >
             <FormField
             <FormField
               control={form.control}
               control={form.control}
@@ -215,7 +215,7 @@ export function RedemptionsMutateDrawer({
                         placeholder={t('Never expires')}
                         placeholder={t('Never expires')}
                       />
                       />
                     </FormControl>
                     </FormControl>
-                    <div className='flex gap-2'>
+                    <div className='grid grid-cols-4 gap-1.5 sm:flex sm:gap-2'>
                       <Button
                       <Button
                         type='button'
                         type='button'
                         variant='outline'
                         variant='outline'
@@ -287,7 +287,7 @@ export function RedemptionsMutateDrawer({
             )}
             )}
           </form>
           </form>
         </Form>
         </Form>
-        <SheetFooter className='gap-2'>
+        <SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
           <SheetClose asChild>
           <SheetClose asChild>
             <Button variant='outline'>{t('Close')}</Button>
             <Button variant='outline'>{t('Close')}</Button>
           </SheetClose>
           </SheetClose>

+ 2 - 2
web/default/src/features/redemption-codes/components/redemptions-table.tsx

@@ -72,7 +72,7 @@ export function RedemptionsTable() {
   } = useTableUrlState({
   } = useTableUrlState({
     search: route.useSearch(),
     search: route.useSearch(),
     navigate: route.useNavigate(),
     navigate: route.useNavigate(),
-    pagination: { defaultPage: 1, defaultPageSize: 20 },
+    pagination: { defaultPage: 1, defaultPageSize: isMobile ? 10 : 20 },
     globalFilter: { enabled: true, key: 'filter' },
     globalFilter: { enabled: true, key: 'filter' },
     columnFilters: [{ columnId: 'status', searchKey: 'status', type: 'array' }],
     columnFilters: [{ columnId: 'status', searchKey: 'status', type: 'array' }],
   })
   })
@@ -154,7 +154,7 @@ export function RedemptionsTable() {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         <DataTableToolbar
         <DataTableToolbar
           table={table}
           table={table}
           searchPlaceholder={t('Filter by name or ID...')}
           searchPlaceholder={t('Filter by name or ID...')}

+ 5 - 5
web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx

@@ -165,7 +165,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
 
 
   return (
   return (
     <Dialog open={props.open} onOpenChange={props.onOpenChange}>
     <Dialog open={props.open} onOpenChange={props.onOpenChange}>
-      <DialogContent className='sm:max-w-md'>
+      <DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle className='flex items-center gap-2'>
           <DialogTitle className='flex items-center gap-2'>
             <Crown className='h-5 w-5' />
             <Crown className='h-5 w-5' />
@@ -173,8 +173,8 @@ export function SubscriptionPurchaseDialog(props: Props) {
           </DialogTitle>
           </DialogTitle>
         </DialogHeader>
         </DialogHeader>
 
 
-        <div className='space-y-4'>
-          <div className='bg-muted/50 space-y-3 rounded-lg border p-4'>
+        <div className='space-y-3 sm:space-y-4'>
+          <div className='bg-muted/50 space-y-2.5 rounded-lg border p-3 sm:space-y-3 sm:p-4'>
             <div className='flex justify-between'>
             <div className='flex justify-between'>
               <span className='text-muted-foreground text-sm'>
               <span className='text-muted-foreground text-sm'>
                 {t('Plan Name')}
                 {t('Plan Name')}
@@ -239,7 +239,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
                 {t('Select payment method')}
                 {t('Select payment method')}
               </p>
               </p>
               {(hasStripe || hasCreem) && (
               {(hasStripe || hasCreem) && (
-                <div className='flex gap-2'>
+                <div className='grid grid-cols-2 gap-2 sm:flex'>
                   {hasStripe && (
                   {hasStripe && (
                     <Button
                     <Button
                       variant='outline'
                       variant='outline'
@@ -263,7 +263,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
                 </div>
                 </div>
               )}
               )}
               {hasEpay && (
               {hasEpay && (
-                <div className='flex gap-2'>
+                <div className='grid grid-cols-[minmax(0,1fr)_auto] gap-2'>
                   <Select
                   <Select
                     value={selectedEpayMethod}
                     value={selectedEpayMethod}
                     onValueChange={setSelectedEpayMethod}
                     onValueChange={setSelectedEpayMethod}

+ 9 - 9
web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx

@@ -124,8 +124,8 @@ export function SubscriptionsMutateDrawer({
         }
         }
       }}
       }}
     >
     >
-      <SheetContent className='flex w-full flex-col sm:max-w-[600px]'>
-        <SheetHeader className='text-start'>
+      <SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'>
+        <SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
           <SheetTitle>
           <SheetTitle>
             {isEdit ? t('Update plan info') : t('Create new subscription plan')}
             {isEdit ? t('Update plan info') : t('Create new subscription plan')}
           </SheetTitle>
           </SheetTitle>
@@ -141,7 +141,7 @@ export function SubscriptionsMutateDrawer({
           <form
           <form
             id='subscription-form'
             id='subscription-form'
             onSubmit={form.handleSubmit(onSubmit)}
             onSubmit={form.handleSubmit(onSubmit)}
-            className='flex-1 space-y-6 overflow-y-auto px-4'
+            className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4'
           >
           >
             {/* Basic Info */}
             {/* Basic Info */}
             <div className='space-y-4'>
             <div className='space-y-4'>
@@ -181,7 +181,7 @@ export function SubscriptionsMutateDrawer({
                 )}
                 )}
               />
               />
 
 
-              <div className='grid grid-cols-2 gap-3'>
+              <div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
                 <FormField
                 <FormField
                   control={form.control}
                   control={form.control}
                   name='price_amount'
                   name='price_amount'
@@ -229,7 +229,7 @@ export function SubscriptionsMutateDrawer({
                 />
                 />
               </div>
               </div>
 
 
-              <div className='grid grid-cols-2 gap-3'>
+              <div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
                 <FormField
                 <FormField
                   control={form.control}
                   control={form.control}
                   name='upgrade_group'
                   name='upgrade_group'
@@ -288,7 +288,7 @@ export function SubscriptionsMutateDrawer({
                 />
                 />
               </div>
               </div>
 
 
-              <div className='grid grid-cols-2 gap-3'>
+              <div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
                 <FormField
                 <FormField
                   control={form.control}
                   control={form.control}
                   name='sort_order'
                   name='sort_order'
@@ -336,7 +336,7 @@ export function SubscriptionsMutateDrawer({
                 {t('Duration Settings')}
                 {t('Duration Settings')}
               </h3>
               </h3>
 
 
-              <div className='grid grid-cols-2 gap-3'>
+              <div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
                 <FormField
                 <FormField
                   control={form.control}
                   control={form.control}
                   name='duration_unit'
                   name='duration_unit'
@@ -418,7 +418,7 @@ export function SubscriptionsMutateDrawer({
                 {t('Quota Reset')}
                 {t('Quota Reset')}
               </h3>
               </h3>
 
 
-              <div className='grid grid-cols-2 gap-3'>
+              <div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
                 <FormField
                 <FormField
                   control={form.control}
                   control={form.control}
                   name='quota_reset_period'
                   name='quota_reset_period'
@@ -508,7 +508,7 @@ export function SubscriptionsMutateDrawer({
             </div>
             </div>
           </form>
           </form>
         </Form>
         </Form>
-        <SheetFooter className='gap-2'>
+        <SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
           <SheetClose asChild>
           <SheetClose asChild>
             <Button variant='outline'>{t('Close')}</Button>
             <Button variant='outline'>{t('Close')}</Button>
           </SheetClose>
           </SheetClose>

+ 1 - 1
web/default/src/features/subscriptions/components/subscriptions-table.tsx

@@ -62,7 +62,7 @@ export function SubscriptionsTable() {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         {isMobile ? (
         {isMobile ? (
           <MobileCardList
           <MobileCardList
             table={table}
             table={table}

+ 1 - 1
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx

@@ -769,7 +769,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
           </>
           </>
         )
         )
       },
       },
-      meta: { label: t('Details'), mobileHidden: true },
+      meta: { label: t('Details') },
       size: 180,
       size: 180,
       maxSize: 200,
       maxSize: 200,
     }
     }

+ 7 - 7
web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx

@@ -141,9 +141,9 @@ export function CommonLogsFilterBar({
     !!filters.requestId
     !!filters.requestId
 
 
   return (
   return (
-    <div className='space-y-3'>
+    <div className='space-y-2 sm:space-y-3'>
       {/* Primary filter row */}
       {/* Primary filter row */}
-      <div className='grid grid-cols-2 gap-2 sm:grid-cols-4 lg:grid-cols-[minmax(280px,2fr)_minmax(140px,1fr)_minmax(120px,1fr)_minmax(120px,0.8fr)_auto]'>
+      <div className='grid grid-cols-2 gap-1.5 sm:grid-cols-4 sm:gap-2 lg:grid-cols-[minmax(280px,2fr)_minmax(140px,1fr)_minmax(120px,1fr)_minmax(120px,0.8fr)_auto]'>
         <CompactDateTimeRangePicker
         <CompactDateTimeRangePicker
           start={filters.startTime}
           start={filters.startTime}
           end={filters.endTime}
           end={filters.endTime}
@@ -214,7 +214,7 @@ export function CommonLogsFilterBar({
         )}
         )}
       >
       >
         <div className='min-h-0 overflow-hidden'>
         <div className='min-h-0 overflow-hidden'>
-          <div className='grid grid-cols-2 gap-2 sm:grid-cols-4'>
+          <div className='grid grid-cols-2 gap-1.5 sm:grid-cols-4 sm:gap-2'>
             <Input
             <Input
               placeholder={t('Token Name')}
               placeholder={t('Token Name')}
               type={sensitiveVisible ? 'text' : 'password'}
               type={sensitiveVisible ? 'text' : 'password'}
@@ -257,9 +257,12 @@ export function CommonLogsFilterBar({
       <div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
       <div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
         <div className='flex min-w-0 flex-wrap items-center gap-2 sm:gap-3'>
         <div className='flex min-w-0 flex-wrap items-center gap-2 sm:gap-3'>
           {stats && <div className='min-w-0'>{stats}</div>}
           {stats && <div className='min-w-0'>{stats}</div>}
+        </div>
+
+        <div className='flex shrink-0 items-center gap-2 self-end sm:self-auto'>
           <button
           <button
             type='button'
             type='button'
-            className='text-muted-foreground hover:text-foreground inline-flex h-6 items-center gap-1 rounded px-1 text-xs transition-colors'
+            className='text-muted-foreground hover:text-foreground inline-flex size-8 items-center justify-center rounded-md border transition-colors'
             title={sensitiveVisible ? t('Hide') : t('Show')}
             title={sensitiveVisible ? t('Hide') : t('Show')}
             aria-label={sensitiveVisible ? t('Hide') : t('Show')}
             aria-label={sensitiveVisible ? t('Hide') : t('Show')}
             onClick={() => setSensitiveVisible(!sensitiveVisible)}
             onClick={() => setSensitiveVisible(!sensitiveVisible)}
@@ -270,9 +273,6 @@ export function CommonLogsFilterBar({
               <EyeOff className='size-3.5' />
               <EyeOff className='size-3.5' />
             )}
             )}
           </button>
           </button>
-        </div>
-
-        <div className='flex shrink-0 items-center gap-2 self-end sm:self-auto'>
           <Button
           <Button
             variant='outline'
             variant='outline'
             size='sm'
             size='sm'

+ 22 - 19
web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx

@@ -63,13 +63,13 @@ function DetailRow(props: {
   muted?: boolean
   muted?: boolean
 }) {
 }) {
   return (
   return (
-    <div className='flex items-start gap-3 text-sm'>
-      <span className='text-muted-foreground w-28 shrink-0 text-xs'>
+    <div className='grid min-w-0 grid-cols-[5.25rem_minmax(0,1fr)] gap-2 text-sm sm:grid-cols-[7rem_minmax(0,1fr)] sm:gap-3'>
+      <span className='text-muted-foreground min-w-0 text-xs'>
         {props.label}
         {props.label}
       </span>
       </span>
       <span
       <span
         className={cn(
         className={cn(
-          'min-w-0 text-xs break-words',
+          'min-w-0 max-w-full text-xs break-all sm:break-words',
           props.mono && 'font-mono',
           props.mono && 'font-mono',
           props.muted && 'text-muted-foreground'
           props.muted && 'text-muted-foreground'
         )}
         )}
@@ -88,7 +88,7 @@ function DetailSection(props: {
 }) {
 }) {
   const isDanger = props.variant === 'danger'
   const isDanger = props.variant === 'danger'
   return (
   return (
-    <div className='space-y-1.5'>
+    <div className='min-w-0 space-y-1.5'>
       <Label
       <Label
         className={cn(
         className={cn(
           'flex items-center gap-1.5 text-xs font-semibold',
           'flex items-center gap-1.5 text-xs font-semibold',
@@ -100,7 +100,7 @@ function DetailSection(props: {
       </Label>
       </Label>
       <div
       <div
         className={cn(
         className={cn(
-          'space-y-1.5 rounded-md border p-2.5',
+          'min-w-0 space-y-1 overflow-hidden rounded-md border p-2.5 max-sm:p-2',
           isDanger
           isDanger
             ? 'border-red-200 bg-red-50 dark:border-red-900 dark:bg-red-950/20'
             ? 'border-red-200 bg-red-50 dark:border-red-900 dark:bg-red-950/20'
             : 'bg-muted/30'
             : 'bg-muted/30'
@@ -462,11 +462,12 @@ export function DetailsDialog(props: DetailsDialogProps) {
     <Dialog open={props.open} onOpenChange={props.onOpenChange}>
     <Dialog open={props.open} onOpenChange={props.onOpenChange}>
       <DialogContent
       <DialogContent
         className={cn(
         className={cn(
-          'min-w-0',
+          'min-w-0 overflow-hidden',
+          'max-sm:max-h-[calc(100dvh-1.5rem)] max-sm:w-[calc(100vw-1.5rem)] max-sm:max-w-[calc(100vw-1.5rem)] max-sm:p-4',
           isTieredBilling ? 'sm:max-w-4xl lg:max-w-5xl' : 'sm:max-w-lg'
           isTieredBilling ? 'sm:max-w-4xl lg:max-w-5xl' : 'sm:max-w-lg'
         )}
         )}
       >
       >
-        <DialogHeader>
+        <DialogHeader className='max-sm:gap-1'>
           <DialogTitle className='flex items-center gap-2 text-base'>
           <DialogTitle className='flex items-center gap-2 text-base'>
             {t('Log Details')}
             {t('Log Details')}
             <StatusBadge
             <StatusBadge
@@ -481,10 +482,10 @@ export function DetailsDialog(props: DetailsDialogProps) {
           </DialogDescription>
           </DialogDescription>
         </DialogHeader>
         </DialogHeader>
 
 
-        <ScrollArea className='max-h-[70vh] min-w-0 pr-4'>
-          <div className='min-w-0 space-y-3 py-1'>
+        <ScrollArea className='max-h-[70vh] min-w-0 overflow-hidden pr-2 max-sm:max-h-[calc(100dvh-7rem)] sm:pr-4'>
+          <div className='w-full min-w-0 max-w-full space-y-2.5 overflow-hidden py-1 sm:space-y-3'>
             {/* Overview section - key identifiers */}
             {/* Overview section - key identifiers */}
-            <div className='space-y-1.5'>
+            <div className='min-w-0 space-y-1'>
               {props.log.request_id && (
               {props.log.request_id && (
                 <DetailRow
                 <DetailRow
                   label={t('Request ID')}
                   label={t('Request ID')}
@@ -587,7 +588,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
             {/* Request conversion (admin only, not for refund) */}
             {/* Request conversion (admin only, not for refund) */}
             {showConversion && (
             {showConversion && (
               <DetailSection label={t('Request Conversion')}>
               <DetailSection label={t('Request Conversion')}>
-                <div className='relative'>
+                <div className='relative min-w-0'>
                   <Button
                   <Button
                     variant='ghost'
                     variant='ghost'
                     size='sm'
                     size='sm'
@@ -602,7 +603,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
                       <Copy className='size-3' />
                       <Copy className='size-3' />
                     )}
                     )}
                   </Button>
                   </Button>
-                  <div className='space-y-1 pr-6'>
+                  <div className='min-w-0 space-y-1 pr-6'>
                     {other?.request_path && (
                     {other?.request_path && (
                       <DetailRow
                       <DetailRow
                         label={t('Path')}
                         label={t('Path')}
@@ -610,12 +611,14 @@ export function DetailsDialog(props: DetailsDialogProps) {
                         mono
                         mono
                       />
                       />
                     )}
                     )}
-                    <div className='flex items-center gap-1.5 text-xs'>
+                    <div className='flex min-w-0 items-center gap-1.5 text-xs'>
                       <Route
                       <Route
                         className='text-muted-foreground size-3'
                         className='text-muted-foreground size-3'
                         aria-hidden='true'
                         aria-hidden='true'
                       />
                       />
-                      <span className='break-words'>{conversionLabel}</span>
+                      <span className='min-w-0 break-all sm:break-words'>
+                        {conversionLabel}
+                      </span>
                     </div>
                     </div>
                   </div>
                   </div>
                 </div>
                 </div>
@@ -825,7 +828,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
 
 
             {/* Tiered pricing breakdown (when billing_mode is tiered_expr) */}
             {/* Tiered pricing breakdown (when billing_mode is tiered_expr) */}
             {isTieredBilling && other?.expr_b64 && (
             {isTieredBilling && other?.expr_b64 && (
-              <div className='bg-muted/30 min-w-0 rounded-md border px-3'>
+              <div className='bg-muted/30 min-w-0 overflow-hidden rounded-md border px-3 max-sm:px-2'>
                 <DynamicPricingBreakdown
                 <DynamicPricingBreakdown
                   billingExpr={decodeBillingExprB64(other.expr_b64)}
                   billingExpr={decodeBillingExprB64(other.expr_b64)}
                   matchedTierLabel={other.matched_tier}
                   matchedTierLabel={other.matched_tier}
@@ -964,7 +967,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
                     return (
                     return (
                       <div
                       <div
                         key={idx}
                         key={idx}
-                        className='bg-background/60 flex items-start gap-2 rounded border p-2'
+                        className='bg-background/60 flex min-w-0 flex-col gap-1.5 rounded border p-2 sm:flex-row sm:items-start sm:gap-2'
                       >
                       >
                         <StatusBadge
                         <StatusBadge
                           variant='neutral'
                           variant='neutral'
@@ -972,7 +975,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
                           className='shrink-0 font-medium'
                           className='shrink-0 font-medium'
                           copyable={false}
                           copyable={false}
                         />
                         />
-                        <span className='min-w-0 font-mono text-[11px] leading-relaxed break-words'>
+                        <span className='min-w-0 font-mono text-[11px] leading-relaxed break-all sm:break-words'>
                           {parsed.content}
                           {parsed.content}
                         </span>
                         </span>
                       </div>
                       </div>
@@ -985,7 +988,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
             {details && (
             {details && (
               <div className='space-y-1.5'>
               <div className='space-y-1.5'>
                 <Label className='text-xs font-semibold'>{t('Content')}</Label>
                 <Label className='text-xs font-semibold'>{t('Content')}</Label>
-                <div className='bg-muted/30 relative rounded-md border p-2.5'>
+                <div className='bg-muted/30 relative min-w-0 overflow-hidden rounded-md border p-2.5'>
                   <Button
                   <Button
                     variant='ghost'
                     variant='ghost'
                     size='sm'
                     size='sm'
@@ -1000,7 +1003,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
                       <Copy className='size-3' />
                       <Copy className='size-3' />
                     )}
                     )}
                   </Button>
                   </Button>
-                  <p className='pr-6 text-xs leading-relaxed break-words whitespace-pre-wrap'>
+                  <p className='min-w-0 pr-6 text-xs leading-relaxed break-all whitespace-pre-wrap sm:break-words'>
                     {details}
                     {details}
                   </p>
                   </p>
                 </div>
                 </div>

+ 6 - 6
web/default/src/features/usage-logs/components/dialogs/usage-logs-filter-dialog.tsx

@@ -271,7 +271,7 @@ export function UsageLogsFilterDialog({
 
 
   return (
   return (
     <Dialog open={open} onOpenChange={setOpen}>
     <Dialog open={open} onOpenChange={setOpen}>
-      <DialogContent className='sm:max-w-lg'>
+      <DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-lg'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>
           <DialogTitle>
             {t('Filter')} {t(getLogCategoryLabel(logCategory))} {t('Logs')}
             {t('Filter')} {t(getLogCategoryLabel(logCategory))} {t('Logs')}
@@ -281,15 +281,15 @@ export function UsageLogsFilterDialog({
           </DialogDescription>
           </DialogDescription>
         </DialogHeader>
         </DialogHeader>
 
 
-        <ScrollArea className='max-h-[60vh] pr-4'>
-          <div className='grid gap-4 py-4'>
+        <ScrollArea className='min-h-0 flex-1 pr-3 sm:max-h-[60vh] sm:pr-4'>
+          <div className='grid gap-3 py-3 sm:gap-4 sm:py-4'>
             {/* Quick time range selection */}
             {/* Quick time range selection */}
             <div className='grid gap-2'>
             <div className='grid gap-2'>
               <Label className='flex items-center gap-2'>
               <Label className='flex items-center gap-2'>
                 <Calendar className='h-4 w-4' />
                 <Calendar className='h-4 w-4' />
                 {t('Quick Range')}
                 {t('Quick Range')}
               </Label>
               </Label>
-              <div className='flex gap-2'>
+              <div className='grid grid-cols-2 gap-2 sm:flex'>
                 {TIME_RANGE_PRESETS.map((range) => (
                 {TIME_RANGE_PRESETS.map((range) => (
                   <Button
                   <Button
                     key={range.days}
                     key={range.days}
@@ -314,7 +314,7 @@ export function UsageLogsFilterDialog({
             <SectionDivider label={t('Custom Time Range')} />
             <SectionDivider label={t('Custom Time Range')} />
 
 
             {/* Custom time range */}
             {/* Custom time range */}
-            <div className='grid gap-4'>
+            <div className='grid gap-3 sm:gap-4'>
               <div className='grid gap-2'>
               <div className='grid gap-2'>
                 <Label htmlFor='start_time'>{t('Start Time')}</Label>
                 <Label htmlFor='start_time'>{t('Start Time')}</Label>
                 <DateTimePicker
                 <DateTimePicker
@@ -355,7 +355,7 @@ export function UsageLogsFilterDialog({
           </div>
           </div>
         </ScrollArea>
         </ScrollArea>
 
 
-        <DialogFooter>
+        <DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
           <Button onClick={handleReset} variant='outline' type='button'>
           <Button onClick={handleReset} variant='outline' type='button'>
             <RotateCcw className='mr-2 h-4 w-4' />
             <RotateCcw className='mr-2 h-4 w-4' />
             {t('Reset')}
             {t('Reset')}

+ 3 - 3
web/default/src/features/usage-logs/components/task-logs-filter-bar.tsx

@@ -138,8 +138,8 @@ export function TaskLogsFilterBar(props: TaskLogsFilterBarProps) {
   )
   )
 
 
   return (
   return (
-    <div className='space-y-3'>
-      <div className='grid grid-cols-2 gap-2 lg:grid-cols-[minmax(280px,2fr)_minmax(180px,1fr)_minmax(120px,0.8fr)_auto]'>
+    <div className='space-y-2 sm:space-y-3'>
+      <div className='grid grid-cols-2 gap-1.5 sm:gap-2 lg:grid-cols-[minmax(280px,2fr)_minmax(180px,1fr)_minmax(120px,0.8fr)_auto]'>
         <CompactDateTimeRangePicker
         <CompactDateTimeRangePicker
           start={filters.startTime}
           start={filters.startTime}
           end={filters.endTime}
           end={filters.endTime}
@@ -166,7 +166,7 @@ export function TaskLogsFilterBar(props: TaskLogsFilterBarProps) {
             className='h-9'
             className='h-9'
           />
           />
         )}
         )}
-        <div className='col-span-2 flex shrink-0 items-center justify-end gap-2 lg:col-span-1'>
+        <div className='col-span-2 flex shrink-0 items-center justify-end gap-1.5 sm:gap-2 lg:col-span-1'>
           <Button
           <Button
             variant='outline'
             variant='outline'
             size='sm'
             size='sm'

+ 4 - 4
web/default/src/features/usage-logs/components/usage-logs-table.tsx

@@ -67,7 +67,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
   } = useTableUrlState({
   } = useTableUrlState({
     search: route.useSearch(),
     search: route.useSearch(),
     navigate: route.useNavigate(),
     navigate: route.useNavigate(),
-    pagination: { defaultPage: 1, defaultPageSize: 100 },
+    pagination: { defaultPage: 1, defaultPageSize: isMobile ? 20 : 100 },
     globalFilter: { enabled: false },
     globalFilter: { enabled: false },
     columnFilters: [
     columnFilters: [
       { columnId: 'created_at', searchKey: 'type', type: 'array' as const },
       { columnId: 'created_at', searchKey: 'type', type: 'array' as const },
@@ -185,16 +185,16 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         {logCategory === 'common' ? (
         {logCategory === 'common' ? (
-          <div className='rounded-md border bg-card/50 p-3 shadow-xs'>
+          <div className='rounded-md border bg-card/50 p-2 shadow-xs sm:p-3'>
             <CommonLogsFilterBar
             <CommonLogsFilterBar
               stats={<CommonLogsStats />}
               stats={<CommonLogsStats />}
               viewOptions={<DataTableViewOptions table={table} />}
               viewOptions={<DataTableViewOptions table={table} />}
             />
             />
           </div>
           </div>
         ) : (
         ) : (
-          <div className='rounded-md border bg-card/50 p-3 shadow-xs'>
+          <div className='rounded-md border bg-card/50 p-2 shadow-xs sm:p-3'>
             <TaskLogsFilterBar
             <TaskLogsFilterBar
               logCategory={logCategory}
               logCategory={logCategory}
               viewOptions={<DataTableViewOptions table={table} />}
               viewOptions={<DataTableViewOptions table={table} />}

+ 4 - 4
web/default/src/features/users/components/users-mutate-drawer.tsx

@@ -152,8 +152,8 @@ export function UsersMutateDrawer({
           }
           }
         }}
         }}
       >
       >
-        <SheetContent className='flex w-full flex-col sm:max-w-[600px]'>
-          <SheetHeader className='text-start'>
+        <SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'>
+          <SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
             <SheetTitle>
             <SheetTitle>
               {isUpdate ? t('Update') : t('Create')} {t('User')}
               {isUpdate ? t('Update') : t('Create')} {t('User')}
             </SheetTitle>
             </SheetTitle>
@@ -167,7 +167,7 @@ export function UsersMutateDrawer({
             <form
             <form
               id='user-form'
               id='user-form'
               onSubmit={form.handleSubmit(onSubmit)}
               onSubmit={form.handleSubmit(onSubmit)}
-              className='flex-1 space-y-6 overflow-y-auto px-4'
+              className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4'
             >
             >
               {/* Basic Information */}
               {/* Basic Information */}
               <div className='space-y-4'>
               <div className='space-y-4'>
@@ -396,7 +396,7 @@ export function UsersMutateDrawer({
               )}
               )}
             </form>
             </form>
           </Form>
           </Form>
-          <SheetFooter className='gap-2'>
+          <SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
             <SheetClose asChild>
             <SheetClose asChild>
               <Button variant='outline'>{t('Close')}</Button>
               <Button variant='outline'>{t('Close')}</Button>
             </SheetClose>
             </SheetClose>

+ 2 - 2
web/default/src/features/users/components/users-table.tsx

@@ -74,7 +74,7 @@ export function UsersTable() {
   } = useTableUrlState({
   } = useTableUrlState({
     search: route.useSearch(),
     search: route.useSearch(),
     navigate: route.useNavigate(),
     navigate: route.useNavigate(),
-    pagination: { defaultPage: 1, defaultPageSize: 20 },
+    pagination: { defaultPage: 1, defaultPageSize: isMobile ? 10 : 20 },
     globalFilter: { enabled: true, key: 'filter' },
     globalFilter: { enabled: true, key: 'filter' },
     columnFilters: [
     columnFilters: [
       { columnId: 'status', searchKey: 'status', type: 'array' },
       { columnId: 'status', searchKey: 'status', type: 'array' },
@@ -168,7 +168,7 @@ export function UsersTable() {
 
 
   return (
   return (
     <>
     <>
-      <div className='space-y-4'>
+      <div className='space-y-3 sm:space-y-4'>
         <DataTableToolbar
         <DataTableToolbar
           table={table}
           table={table}
           searchPlaceholder={t('Filter by username, name or email...')}
           searchPlaceholder={t('Filter by username, name or email...')}

+ 57 - 108
web/default/src/features/wallet/components/affiliate-rewards-card.tsx

@@ -2,15 +2,8 @@ import { Share2 } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { formatQuota } from '@/lib/format'
 import { formatQuota } from '@/lib/format'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from '@/components/ui/card'
+import { Card, CardContent } from '@/components/ui/card'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Skeleton } from '@/components/ui/skeleton'
 import { CopyButton } from '@/components/copy-button'
 import { CopyButton } from '@/components/copy-button'
 import type { UserWalletData } from '../types'
 import type { UserWalletData } from '../types'
@@ -31,33 +24,14 @@ export function AffiliateRewardsCard({
   const { t } = useTranslation()
   const { t } = useTranslation()
   if (loading) {
   if (loading) {
     return (
     return (
-      <Card className='overflow-hidden'>
-        <CardHeader className='border-b'>
-          <Skeleton className='h-6 w-32' />
-          <Skeleton className='mt-2 h-4 w-48' />
-        </CardHeader>
-        <CardContent className='space-y-6 pt-6'>
-          {/* Statistics Skeleton */}
-          <div className='grid grid-cols-1 gap-3'>
-            {Array.from({ length: 3 }).map((_, i) => (
-              <div key={i} className='rounded-lg border p-3'>
-                <Skeleton className='h-3 w-16' />
-                <Skeleton className='mt-2 h-8 w-24' />
-              </div>
-            ))}
+      <Card className='bg-muted/20 py-0'>
+        <CardContent className='grid gap-4 p-3 sm:p-4 lg:grid-cols-[minmax(220px,1fr)_minmax(220px,0.72fr)_minmax(320px,1.15fr)] lg:items-center'>
+          <div>
+            <Skeleton className='h-5 w-32' />
+            <Skeleton className='mt-2 h-4 w-48' />
           </div>
           </div>
-
-          {/* Affiliate Link Skeleton */}
-          <div className='space-y-3'>
-            <Skeleton className='h-3 w-32' />
-            <div className='flex gap-2'>
-              <Skeleton className='h-10 flex-1' />
-              <Skeleton className='size-9' />
-            </div>
-          </div>
-
-          {/* Info Section Skeleton */}
-          <Skeleton className='h-20 w-full rounded-lg' />
+          <Skeleton className='h-14 rounded-lg' />
+          <Skeleton className='h-10 rounded-lg' />
         </CardContent>
         </CardContent>
       </Card>
       </Card>
     )
     )
@@ -66,89 +40,64 @@ export function AffiliateRewardsCard({
   const hasRewards = (user?.aff_quota ?? 0) > 0
   const hasRewards = (user?.aff_quota ?? 0) > 0
 
 
   return (
   return (
-    <Card className='overflow-hidden'>
-      <CardHeader className='border-b'>
-        <div className='flex items-center gap-3'>
-          <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
-            <Share2 className='h-4 w-4' />
+    <Card className='bg-muted/20 py-0'>
+      <CardContent className='grid gap-3 p-3 sm:gap-4 sm:p-4 lg:grid-cols-[minmax(200px,1fr)_minmax(180px,0.65fr)_minmax(280px,1fr)] lg:items-center'>
+        <div className='flex min-w-0 items-center gap-2.5'>
+          <div className='bg-background flex size-8 shrink-0 items-center justify-center rounded-lg border'>
+            <Share2 className='text-muted-foreground size-4' />
           </div>
           </div>
           <div className='min-w-0'>
           <div className='min-w-0'>
-            <CardTitle className='text-xl tracking-tight'>
+            <h3 className='truncate text-sm font-semibold'>
               {t('Referral Program')}
               {t('Referral Program')}
-            </CardTitle>
-            <CardDescription>
-              {t('Share your link and earn rewards')}
-            </CardDescription>
+            </h3>
+            <p className='text-muted-foreground line-clamp-1 text-xs'>
+              {t(
+                'Earn rewards when your referrals add funds. Transfer accumulated rewards to your balance anytime.'
+              )}
+            </p>
           </div>
           </div>
         </div>
         </div>
-      </CardHeader>
-      <CardContent className='space-y-6 pt-6'>
-        {/* Statistics */}
-        <div className='grid grid-cols-1 gap-3 sm:grid-cols-3 xl:grid-cols-1'>
-          <div className='rounded-lg border p-3'>
-            <div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
-              {t('Pending')}
-            </div>
-            <div className='mt-2 text-2xl font-semibold break-all'>
-              {formatQuota(user?.aff_quota ?? 0)}
-            </div>
-          </div>
-
-          <div className='rounded-lg border p-3'>
-            <div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
-              {t('Total Earned')}
-            </div>
-            <div className='mt-2 text-2xl font-semibold break-all'>
-              {formatQuota(user?.aff_history_quota ?? 0)}
-            </div>
-          </div>
 
 
-          <div className='rounded-lg border p-3'>
-            <div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
-              {t('Invites')}
-            </div>
-            <div className='mt-2 text-2xl font-semibold'>
-              {user?.aff_count ?? 0}
+        <div className='grid grid-cols-3 gap-1.5 text-center'>
+          {[
+            [t('Pending'), formatQuota(user?.aff_quota ?? 0)],
+            [t('Total Earned'), formatQuota(user?.aff_history_quota ?? 0)],
+            [t('Invites'), String(user?.aff_count ?? 0)],
+          ].map(([label, value]) => (
+            <div key={label}>
+              <div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
+                {label}
+              </div>
+              <div className='mt-0.5 truncate text-sm font-semibold tabular-nums'>
+                {value}
+              </div>
             </div>
             </div>
-          </div>
-        </div>
-
-        {/* Transfer Button */}
-        {hasRewards && (
-          <Button onClick={onTransfer} className='w-full' variant='default'>
-            {t('Transfer to Balance')}
-          </Button>
-        )}
-
-        {/* Affiliate Link */}
-        <div className='space-y-3'>
-          <Label className='text-muted-foreground text-xs tracking-wider uppercase'>
-            {t('Your Referral Link')}
-          </Label>
-          <div className='flex gap-2'>
-            <Input
-              value={affiliateLink}
-              readOnly
-              className='border-muted bg-muted/30 font-mono text-sm'
-            />
-            <CopyButton
-              value={affiliateLink}
-              variant='outline'
-              className='size-9'
-              iconClassName='size-4'
-              tooltip={t('Copy referral link')}
-              aria-label={t('Copy referral link')}
-            />
-          </div>
+          ))}
         </div>
         </div>
 
 
-        {/* Info */}
-        <div className='bg-muted/30 space-y-2 rounded-lg p-4'>
-          <p className='text-muted-foreground text-sm leading-relaxed'>
-            {t(
-              'Earn rewards when your referrals add funds. Transfer accumulated rewards to your balance anytime.'
-            )}
-          </p>
+        <div className='flex items-center gap-2'>
+          <Input
+            value={affiliateLink}
+            readOnly
+            className='border-muted bg-background/70 h-9 min-w-0 flex-1 font-mono text-xs'
+          />
+          <CopyButton
+            value={affiliateLink}
+            variant='outline'
+            className='size-9 shrink-0 bg-background'
+            iconClassName='size-4'
+            tooltip={t('Copy referral link')}
+            aria-label={t('Copy referral link')}
+          />
+          {hasRewards && (
+            <Button
+              onClick={onTransfer}
+              className='h-9 shrink-0 px-3'
+              size='sm'
+            >
+              {t('Transfer to Balance')}
+            </Button>
+          )}
         </div>
         </div>
       </CardContent>
       </CardContent>
     </Card>
     </Card>

+ 3 - 3
web/default/src/features/wallet/components/creem-products-section.tsx

@@ -20,7 +20,7 @@ export function CreemProductsSection({
 
 
   if (loading) {
   if (loading) {
     return (
     return (
-      <div className='grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3'>
+      <div className='grid grid-cols-2 gap-2 sm:grid-cols-2 sm:gap-3 md:grid-cols-3'>
         {Array.from({ length: 3 }).map((_, i) => (
         {Array.from({ length: 3 }).map((_, i) => (
           <Skeleton key={i} className='h-24 rounded-lg' />
           <Skeleton key={i} className='h-24 rounded-lg' />
         ))}
         ))}
@@ -33,14 +33,14 @@ export function CreemProductsSection({
   }
   }
 
 
   return (
   return (
-    <div className='grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3'>
+    <div className='grid grid-cols-2 gap-2 sm:grid-cols-2 sm:gap-3 md:grid-cols-3'>
       {products.map((product) => (
       {products.map((product) => (
         <Card
         <Card
           key={product.productId}
           key={product.productId}
           className='hover:border-foreground/50 cursor-pointer transition-all hover:shadow-md'
           className='hover:border-foreground/50 cursor-pointer transition-all hover:shadow-md'
           onClick={() => onProductSelect(product)}
           onClick={() => onProductSelect(product)}
         >
         >
-          <CardContent className='p-4 text-center'>
+          <CardContent className='p-3 text-center sm:p-4'>
             <div className='mb-2 text-lg font-medium'>{product.name}</div>
             <div className='mb-2 text-lg font-medium'>{product.name}</div>
             <div className='text-muted-foreground mb-2 text-sm'>
             <div className='text-muted-foreground mb-2 text-sm'>
               {t('Quota')}: {formatNumber(product.quota)}
               {t('Quota')}: {formatNumber(product.quota)}

+ 13 - 13
web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx

@@ -83,7 +83,7 @@ export function BillingHistoryDialog({
   return (
   return (
     <>
     <>
       <Dialog open={open} onOpenChange={onOpenChange}>
       <Dialog open={open} onOpenChange={onOpenChange}>
-        <DialogContent className='max-w-4xl'>
+        <DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-4xl'>
           <DialogHeader>
           <DialogHeader>
             <DialogTitle>{t('Billing History')}</DialogTitle>
             <DialogTitle>{t('Billing History')}</DialogTitle>
             <DialogDescription>
             <DialogDescription>
@@ -91,7 +91,7 @@ export function BillingHistoryDialog({
             </DialogDescription>
             </DialogDescription>
           </DialogHeader>
           </DialogHeader>
 
 
-          <div className='space-y-4'>
+          <div className='min-h-0 flex-1 space-y-3 sm:space-y-4'>
             {/* Search and Filter Bar */}
             {/* Search and Filter Bar */}
             <div className='flex items-center gap-2'>
             <div className='flex items-center gap-2'>
               <div className='relative flex-1'>
               <div className='relative flex-1'>
@@ -100,14 +100,14 @@ export function BillingHistoryDialog({
                   placeholder={t('Search by order number...')}
                   placeholder={t('Search by order number...')}
                   value={keyword}
                   value={keyword}
                   onChange={(e) => handleSearch(e.target.value)}
                   onChange={(e) => handleSearch(e.target.value)}
-                  className='pl-10'
+                  className='h-9 pl-10'
                 />
                 />
               </div>
               </div>
               <Select
               <Select
                 value={pageSize.toString()}
                 value={pageSize.toString()}
                 onValueChange={(value) => handlePageSizeChange(parseInt(value))}
                 onValueChange={(value) => handlePageSizeChange(parseInt(value))}
               >
               >
-                <SelectTrigger className='w-32'>
+                <SelectTrigger className='h-9 w-[92px] sm:w-32'>
                   <SelectValue />
                   <SelectValue />
                 </SelectTrigger>
                 </SelectTrigger>
                 <SelectContent>
                 <SelectContent>
@@ -120,11 +120,11 @@ export function BillingHistoryDialog({
             </div>
             </div>
 
 
             {/* Records List */}
             {/* Records List */}
-            <ScrollArea className='h-[500px] pr-4'>
+            <ScrollArea className='h-[calc(100dvh-15rem)] pr-3 sm:h-[500px] sm:pr-4'>
               {loading ? (
               {loading ? (
                 <div className='space-y-3'>
                 <div className='space-y-3'>
                   {Array.from({ length: 5 }).map((_, i) => (
                   {Array.from({ length: 5 }).map((_, i) => (
-                    <div key={i} className='rounded-lg border p-4'>
+                    <div key={i} className='rounded-lg border p-3 sm:p-4'>
                       <div className='flex items-start justify-between'>
                       <div className='flex items-start justify-between'>
                         <div className='flex-1 space-y-2'>
                         <div className='flex-1 space-y-2'>
                           <Skeleton className='h-4 w-48' />
                           <Skeleton className='h-4 w-48' />
@@ -132,7 +132,7 @@ export function BillingHistoryDialog({
                         </div>
                         </div>
                         <Skeleton className='h-5 w-16' />
                         <Skeleton className='h-5 w-16' />
                       </div>
                       </div>
-                      <div className='mt-3 grid grid-cols-3 gap-4'>
+                      <div className='mt-3 grid grid-cols-2 gap-3 sm:grid-cols-3 sm:gap-4'>
                         <Skeleton className='h-3 w-full' />
                         <Skeleton className='h-3 w-full' />
                         <Skeleton className='h-3 w-full' />
                         <Skeleton className='h-3 w-full' />
                         <Skeleton className='h-3 w-full' />
                         <Skeleton className='h-3 w-full' />
@@ -141,7 +141,7 @@ export function BillingHistoryDialog({
                   ))}
                   ))}
                 </div>
                 </div>
               ) : records.length === 0 ? (
               ) : records.length === 0 ? (
-                <div className='text-muted-foreground flex h-[400px] flex-col items-center justify-center text-center'>
+                <div className='text-muted-foreground flex h-[320px] flex-col items-center justify-center text-center sm:h-[400px]'>
                   <p className='text-sm font-medium'>
                   <p className='text-sm font-medium'>
                     {t('No billing records found')}
                     {t('No billing records found')}
                   </p>
                   </p>
@@ -158,13 +158,13 @@ export function BillingHistoryDialog({
                     return (
                     return (
                       <div
                       <div
                         key={record.id}
                         key={record.id}
-                        className='hover:bg-muted/50 rounded-lg border p-4 transition-colors'
+                        className='hover:bg-muted/50 rounded-lg border p-3 transition-colors sm:p-4'
                       >
                       >
                         {/* Header Row */}
                         {/* Header Row */}
-                        <div className='flex items-start justify-between'>
+                        <div className='flex items-start justify-between gap-2'>
                           <div className='flex-1 space-y-1'>
                           <div className='flex-1 space-y-1'>
-                            <div className='flex items-center gap-2'>
-                              <code className='text-foreground font-mono text-sm'>
+                            <div className='flex min-w-0 items-center gap-2'>
+                              <code className='text-foreground truncate font-mono text-sm'>
                                 {record.trade_no}
                                 {record.trade_no}
                               </code>
                               </code>
                               <Button
                               <Button
@@ -201,7 +201,7 @@ export function BillingHistoryDialog({
                         </div>
                         </div>
 
 
                         {/* Details Grid */}
                         {/* Details Grid */}
-                        <div className='mt-4 grid grid-cols-3 gap-4'>
+                        <div className='mt-3 grid grid-cols-2 gap-3 sm:mt-4 sm:grid-cols-3 sm:gap-4'>
                           <div className='space-y-1'>
                           <div className='space-y-1'>
                             <Label className='text-muted-foreground text-xs'>
                             <Label className='text-muted-foreground text-xs'>
                               Payment Method
                               Payment Method

+ 3 - 3
web/default/src/features/wallet/components/dialogs/creem-confirm-dialog.tsx

@@ -34,7 +34,7 @@ export function CreemConfirmDialog({
 
 
   return (
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className='sm:max-w-[425px]'>
+      <DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-[425px]'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t('Confirm Creem Purchase')}</DialogTitle>
           <DialogTitle>{t('Confirm Creem Purchase')}</DialogTitle>
           <DialogDescription>
           <DialogDescription>
@@ -42,7 +42,7 @@ export function CreemConfirmDialog({
           </DialogDescription>
           </DialogDescription>
         </DialogHeader>
         </DialogHeader>
 
 
-        <div className='space-y-4 py-4'>
+        <div className='space-y-3 py-3 sm:space-y-4 sm:py-4'>
           <div className='flex items-center justify-between'>
           <div className='flex items-center justify-between'>
             <span className='text-muted-foreground'>{t('Product')}</span>
             <span className='text-muted-foreground'>{t('Product')}</span>
             <span className='font-medium'>{product.name}</span>
             <span className='font-medium'>{product.name}</span>
@@ -59,7 +59,7 @@ export function CreemConfirmDialog({
           </div>
           </div>
         </div>
         </div>
 
 
-        <DialogFooter>
+        <DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
           <Button
           <Button
             variant='outline'
             variant='outline'
             onClick={() => onOpenChange(false)}
             onClick={() => onOpenChange(false)}

+ 3 - 3
web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx

@@ -48,7 +48,7 @@ export function PaymentConfirmDialog({
 
 
   return (
   return (
     <AlertDialog open={open} onOpenChange={onOpenChange}>
     <AlertDialog open={open} onOpenChange={onOpenChange}>
-      <AlertDialogContent className='max-w-md'>
+      <AlertDialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'>
         <AlertDialogHeader>
         <AlertDialogHeader>
           <AlertDialogTitle className='text-xl font-semibold'>
           <AlertDialogTitle className='text-xl font-semibold'>
             {t('Confirm Payment')}
             {t('Confirm Payment')}
@@ -58,7 +58,7 @@ export function PaymentConfirmDialog({
           </AlertDialogDescription>
           </AlertDialogDescription>
         </AlertDialogHeader>
         </AlertDialogHeader>
 
 
-        <div className='space-y-4 py-4'>
+        <div className='space-y-3 py-3 sm:space-y-4 sm:py-4'>
           <div className='flex items-center justify-between'>
           <div className='flex items-center justify-between'>
             <span className='text-muted-foreground text-sm'>
             <span className='text-muted-foreground text-sm'>
               {t('Topup Amount')}
               {t('Topup Amount')}
@@ -121,7 +121,7 @@ export function PaymentConfirmDialog({
           </div>
           </div>
         </div>
         </div>
 
 
-        <AlertDialogFooter>
+        <AlertDialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
           <AlertDialogCancel disabled={processing}>
           <AlertDialogCancel disabled={processing}>
             {t('Cancel')}
             {t('Cancel')}
           </AlertDialogCancel>
           </AlertDialogCancel>

+ 3 - 3
web/default/src/features/wallet/components/dialogs/transfer-dialog.tsx

@@ -49,7 +49,7 @@ export function TransferDialog({
 
 
   return (
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className='max-w-md'>
+      <DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle className='text-xl font-semibold'>
           <DialogTitle className='text-xl font-semibold'>
             {t('Transfer Rewards')}
             {t('Transfer Rewards')}
@@ -59,7 +59,7 @@ export function TransferDialog({
           </DialogDescription>
           </DialogDescription>
         </DialogHeader>
         </DialogHeader>
 
 
-        <div className='space-y-6 py-4'>
+        <div className='space-y-4 py-3 sm:space-y-6 sm:py-4'>
           <div className='space-y-2'>
           <div className='space-y-2'>
             <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
             <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
               {t('Available Rewards')}
               {t('Available Rewards')}
@@ -92,7 +92,7 @@ export function TransferDialog({
           </div>
           </div>
         </div>
         </div>
 
 
-        <DialogFooter>
+        <DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
           <Button
           <Button
             variant='outline'
             variant='outline'
             onClick={() => onOpenChange(false)}
             onClick={() => onOpenChange(false)}

+ 40 - 53
web/default/src/features/wallet/components/recharge-form-card.tsx

@@ -8,13 +8,12 @@ import { Button } from '@/components/ui/button'
 import {
 import {
   Card,
   Card,
   CardContent,
   CardContent,
-  CardDescription,
   CardHeader,
   CardHeader,
-  CardTitle,
 } from '@/components/ui/card'
 } from '@/components/ui/card'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Label } from '@/components/ui/label'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Skeleton } from '@/components/ui/skeleton'
+import { TitledCard } from '@/components/ui/titled-card'
 import {
 import {
   Tooltip,
   Tooltip,
   TooltipContent,
   TooltipContent,
@@ -125,13 +124,13 @@ export function RechargeFormCard({
 
 
   if (loading) {
   if (loading) {
     return (
     return (
-      <Card className='overflow-hidden'>
-        <CardHeader className='border-b'>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='h-6 w-32' />
           <Skeleton className='mt-2 h-4 w-48' />
           <Skeleton className='mt-2 h-4 w-48' />
         </CardHeader>
         </CardHeader>
-        <CardContent className='space-y-6 pt-6'>
-          <div className='space-y-6'>
+        <CardContent className='space-y-4 p-3 sm:space-y-6 sm:p-5'>
+          <div className='space-y-4 sm:space-y-6'>
             {/* Preset Amounts Skeleton */}
             {/* Preset Amounts Skeleton */}
             <div className='space-y-3'>
             <div className='space-y-3'>
               <Skeleton className='h-3 w-16' />
               <Skeleton className='h-3 w-16' />
@@ -173,23 +172,12 @@ export function RechargeFormCard({
   }
   }
 
 
   return (
   return (
-    <Card className='overflow-hidden'>
-      <CardHeader className='border-b'>
-        <div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
-          <div className='flex min-w-0 items-center gap-3'>
-            <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
-              <WalletCards className='h-4 w-4' />
-            </div>
-            <div className='min-w-0'>
-              <CardTitle className='text-xl tracking-tight'>
-                {t('Add Funds')}
-              </CardTitle>
-              <CardDescription>
-                {t('Choose an amount and payment method')}
-              </CardDescription>
-            </div>
-          </div>
-          {onOpenBilling && (
+    <TitledCard
+      title={t('Add Funds')}
+      description={t('Choose an amount and payment method')}
+      icon={<WalletCards className='h-4 w-4' />}
+      action={
+        onOpenBilling ? (
             <Button
             <Button
               variant='outline'
               variant='outline'
               size='sm'
               size='sm'
@@ -199,21 +187,21 @@ export function RechargeFormCard({
               <Receipt className='h-4 w-4' />
               <Receipt className='h-4 w-4' />
               {t('Order History')}
               {t('Order History')}
             </Button>
             </Button>
-          )}
-        </div>
-      </CardHeader>
-      <CardContent className='space-y-6 pt-6'>
+        ) : null
+      }
+      contentClassName='space-y-4 sm:space-y-6'
+    >
         {/* Online Topup Section */}
         {/* Online Topup Section */}
         {hasAnyTopup ? (
         {hasAnyTopup ? (
-          <div className='space-y-6'>
+          <div className='space-y-4 sm:space-y-6'>
             {hasConfigurableTopup && (
             {hasConfigurableTopup && (
               <>
               <>
                 {presetAmounts.length > 0 && (
                 {presetAmounts.length > 0 && (
-                  <div className='space-y-3'>
+                  <div className='space-y-2.5 sm:space-y-3'>
                     <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                     <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                       {t('Amount')}
                       {t('Amount')}
                     </Label>
                     </Label>
-                    <div className='grid grid-cols-2 gap-3 md:grid-cols-4'>
+                    <div className='grid grid-cols-2 gap-1.5 sm:gap-3 md:grid-cols-4'>
                       {presetAmounts.map((preset, index) => {
                       {presetAmounts.map((preset, index) => {
                         const discount =
                         const discount =
                           preset.discount ||
                           preset.discount ||
@@ -235,7 +223,7 @@ export function RechargeFormCard({
                             key={index}
                             key={index}
                             variant='outline'
                             variant='outline'
                             className={cn(
                             className={cn(
-                              'hover:border-foreground flex h-auto flex-col items-start rounded-lg p-4 text-left whitespace-normal',
+                              'hover:border-foreground flex min-h-16 flex-col items-start rounded-lg px-3 py-2.5 text-left whitespace-normal sm:min-h-[72px] sm:p-4',
                               selectedPreset === preset.value
                               selectedPreset === preset.value
                                 ? 'border-foreground bg-foreground/5'
                                 ? 'border-foreground bg-foreground/5'
                                 : 'border-muted'
                                 : 'border-muted'
@@ -243,7 +231,7 @@ export function RechargeFormCard({
                             onClick={() => onSelectPreset(preset)}
                             onClick={() => onSelectPreset(preset)}
                           >
                           >
                             <div className='flex w-full items-center justify-between'>
                             <div className='flex w-full items-center justify-between'>
-                              <div className='text-lg font-semibold'>
+                              <div className='text-base font-semibold sm:text-lg'>
                                 {formatNumber(displayValue)}
                                 {formatNumber(displayValue)}
                               </div>
                               </div>
                               {hasDiscount && (
                               {hasDiscount && (
@@ -252,7 +240,7 @@ export function RechargeFormCard({
                                 </div>
                                 </div>
                               )}
                               )}
                             </div>
                             </div>
-                            <div className='text-muted-foreground mt-2 w-full text-xs'>
+                            <div className='text-muted-foreground mt-1.5 w-full text-xs sm:mt-2'>
                               Pay {formatCurrency(actualPrice)}
                               Pay {formatCurrency(actualPrice)}
                               {hasDiscount && savedAmount > 0 && (
                               {hasDiscount && savedAmount > 0 && (
                                 <span className='text-green-600'>
                                 <span className='text-green-600'>
@@ -268,14 +256,14 @@ export function RechargeFormCard({
                   </div>
                   </div>
                 )}
                 )}
 
 
-                <div className='space-y-3'>
+                <div className='space-y-2.5 sm:space-y-3'>
                   <Label
                   <Label
                     htmlFor='topup-amount'
                     htmlFor='topup-amount'
                     className='text-muted-foreground text-xs font-medium tracking-wider uppercase'
                     className='text-muted-foreground text-xs font-medium tracking-wider uppercase'
                   >
                   >
                     {t('Custom Amount')}
                     {t('Custom Amount')}
                   </Label>
                   </Label>
-                  <div className='grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center'>
+                  <div className='grid grid-cols-[minmax(0,1fr)_minmax(110px,0.55fr)] gap-2 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center'>
                     <Input
                     <Input
                       id='topup-amount'
                       id='topup-amount'
                       type='number'
                       type='number'
@@ -283,10 +271,10 @@ export function RechargeFormCard({
                       onChange={(e) => handleAmountChange(e.target.value)}
                       onChange={(e) => handleAmountChange(e.target.value)}
                       min={minTopup}
                       min={minTopup}
                       placeholder={`Minimum ${minTopup}`}
                       placeholder={`Minimum ${minTopup}`}
-                      className='text-lg'
+                      className='h-9 text-base sm:h-10 sm:text-lg'
                     />
                     />
-                    <div className='bg-muted/30 flex min-h-10 items-center justify-between gap-3 rounded-md border px-3 lg:min-w-52'>
-                      <span className='text-muted-foreground text-xs'>
+                    <div className='bg-muted/30 flex min-h-9 items-center justify-between gap-2 rounded-md border px-3 lg:min-w-52'>
+                      <span className='text-muted-foreground truncate text-xs'>
                         {t('Amount to pay:')}
                         {t('Amount to pay:')}
                       </span>
                       </span>
                       {calculating ? (
                       {calculating ? (
@@ -300,12 +288,12 @@ export function RechargeFormCard({
                   </div>
                   </div>
                 </div>
                 </div>
 
 
-                <div className='space-y-3'>
+                <div className='space-y-2.5 sm:space-y-3'>
                   <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                   <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                     {t('Payment Method')}
                     {t('Payment Method')}
                   </Label>
                   </Label>
                   {hasStandardPaymentMethods ? (
                   {hasStandardPaymentMethods ? (
-                    <div className='grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3'>
+                    <div className='grid grid-cols-2 gap-1.5 sm:gap-3 lg:grid-cols-3'>
                       {topupInfo?.pay_methods?.map((method) => {
                       {topupInfo?.pay_methods?.map((method) => {
                         const minTopup = method.min_topup || 0
                         const minTopup = method.min_topup || 0
                         const disabled = minTopup > topupAmount
                         const disabled = minTopup > topupAmount
@@ -316,7 +304,7 @@ export function RechargeFormCard({
                             variant='outline'
                             variant='outline'
                             onClick={() => onPaymentMethodSelect(method)}
                             onClick={() => onPaymentMethodSelect(method)}
                             disabled={disabled || !!paymentLoading}
                             disabled={disabled || !!paymentLoading}
-                            className='justify-start gap-2 rounded-lg'
+                            className='h-9 min-w-0 justify-start gap-2 rounded-lg px-3'
                           >
                           >
                             {paymentLoading === method.type ? (
                             {paymentLoading === method.type ? (
                               <Loader2 className='h-4 w-4 animate-spin' />
                               <Loader2 className='h-4 w-4 animate-spin' />
@@ -328,7 +316,7 @@ export function RechargeFormCard({
                                 method.name
                                 method.name
                               )
                               )
                             )}
                             )}
-                            {method.name}
+                            <span className='truncate'>{method.name}</span>
                           </Button>
                           </Button>
                         )
                         )
 
 
@@ -362,11 +350,11 @@ export function RechargeFormCard({
                 {enableWaffoTopup &&
                 {enableWaffoTopup &&
                   hasWaffoPaymentMethods &&
                   hasWaffoPaymentMethods &&
                   onWaffoMethodSelect && (
                   onWaffoMethodSelect && (
-                    <div className='space-y-3'>
+                    <div className='space-y-2.5 sm:space-y-3'>
                       <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                       <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                         {t('Waffo Payment')}
                         {t('Waffo Payment')}
                       </Label>
                       </Label>
-                      <div className='grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3'>
+                      <div className='grid grid-cols-2 gap-1.5 sm:gap-3 lg:grid-cols-3'>
                         {waffoPayMethods?.map((method, index) => {
                         {waffoPayMethods?.map((method, index) => {
                           const loadingKey = `waffo-${index}`
                           const loadingKey = `waffo-${index}`
                           const waffoMin = waffoMinTopup || 0
                           const waffoMin = waffoMinTopup || 0
@@ -378,7 +366,7 @@ export function RechargeFormCard({
                               variant='outline'
                               variant='outline'
                               onClick={() => onWaffoMethodSelect(method, index)}
                               onClick={() => onWaffoMethodSelect(method, index)}
                               disabled={belowMin || !!paymentLoading}
                               disabled={belowMin || !!paymentLoading}
-                              className='justify-start gap-2 rounded-lg'
+                              className='h-9 min-w-0 justify-start gap-2 rounded-lg px-3'
                             >
                             >
                               {paymentLoading === loadingKey ? (
                               {paymentLoading === loadingKey ? (
                                 <Loader2 className='h-4 w-4 animate-spin' />
                                 <Loader2 className='h-4 w-4 animate-spin' />
@@ -391,7 +379,7 @@ export function RechargeFormCard({
                               ) : (
                               ) : (
                                 getPaymentIcon('waffo')
                                 getPaymentIcon('waffo')
                               )}
                               )}
-                              {method.name}
+                              <span className='truncate'>{method.name}</span>
                             </Button>
                             </Button>
                           )
                           )
 
 
@@ -433,7 +421,7 @@ export function RechargeFormCard({
           Array.isArray(creemProducts) &&
           Array.isArray(creemProducts) &&
           creemProducts.length > 0 &&
           creemProducts.length > 0 &&
           onCreemProductSelect && (
           onCreemProductSelect && (
-            <div className='space-y-3 border-t pt-6'>
+            <div className='space-y-2.5 border-t pt-4 sm:space-y-3 sm:pt-6'>
               <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
               <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                 {t('Creem Payment')}
                 {t('Creem Payment')}
               </Label>
               </Label>
@@ -445,7 +433,7 @@ export function RechargeFormCard({
           )}
           )}
 
 
         {/* Redemption Code Section */}
         {/* Redemption Code Section */}
-        <div className='space-y-3 border-t pt-6'>
+        <div className='space-y-2.5 border-t pt-4 sm:space-y-3 sm:pt-6'>
           <div className='flex items-center gap-2'>
           <div className='flex items-center gap-2'>
             <Gift className='text-muted-foreground h-4 w-4' />
             <Gift className='text-muted-foreground h-4 w-4' />
             <Label
             <Label
@@ -455,19 +443,19 @@ export function RechargeFormCard({
               {t('Have a Code?')}
               {t('Have a Code?')}
             </Label>
             </Label>
           </div>
           </div>
-          <div className='flex flex-col gap-2 sm:flex-row'>
+          <div className='grid grid-cols-[minmax(0,1fr)_auto] gap-2'>
             <Input
             <Input
               id='redemption-code'
               id='redemption-code'
               value={redemptionCode}
               value={redemptionCode}
               onChange={(e) => onRedemptionCodeChange(e.target.value)}
               onChange={(e) => onRedemptionCodeChange(e.target.value)}
               placeholder={t('Enter your redemption code')}
               placeholder={t('Enter your redemption code')}
-              className='flex-1'
+              className='h-9 min-w-0'
             />
             />
             <Button
             <Button
               onClick={onRedeem}
               onClick={onRedeem}
               disabled={redeeming}
               disabled={redeeming}
               variant='outline'
               variant='outline'
-              className='sm:w-auto'
+              className='h-9 px-4'
             >
             >
               {redeeming && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
               {redeeming && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
               {t('Redeem')}
               {t('Redeem')}
@@ -488,7 +476,6 @@ export function RechargeFormCard({
             </p>
             </p>
           )}
           )}
         </div>
         </div>
-      </CardContent>
-    </Card>
+    </TitledCard>
   )
   )
 }
 }

+ 31 - 35
web/default/src/features/wallet/components/subscription-plans-card.tsx

@@ -9,9 +9,7 @@ import { Button } from '@/components/ui/button'
 import {
 import {
   Card,
   Card,
   CardContent,
   CardContent,
-  CardDescription,
   CardHeader,
   CardHeader,
-  CardTitle,
 } from '@/components/ui/card'
 } from '@/components/ui/card'
 import { Progress } from '@/components/ui/progress'
 import { Progress } from '@/components/ui/progress'
 import {
 import {
@@ -23,6 +21,7 @@ import {
 } from '@/components/ui/select'
 } from '@/components/ui/select'
 import { Separator } from '@/components/ui/separator'
 import { Separator } from '@/components/ui/separator'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Skeleton } from '@/components/ui/skeleton'
+import { TitledCard } from '@/components/ui/titled-card'
 import {
 import {
   Tooltip,
   Tooltip,
   TooltipContent,
   TooltipContent,
@@ -48,6 +47,7 @@ import type { PaymentMethod, TopupInfo } from '../types'
 
 
 interface SubscriptionPlansCardProps {
 interface SubscriptionPlansCardProps {
   topupInfo: TopupInfo | null
   topupInfo: TopupInfo | null
+  onAvailabilityChange?: (available: boolean) => void
 }
 }
 
 
 function getEpayMethods(payMethods: PaymentMethod[] = []): PaymentMethod[] {
 function getEpayMethods(payMethods: PaymentMethod[] = []): PaymentMethod[] {
@@ -56,7 +56,10 @@ function getEpayMethods(payMethods: PaymentMethod[] = []): PaymentMethod[] {
   )
   )
 }
 }
 
 
-export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
+export function SubscriptionPlansCard({
+  topupInfo,
+  onAvailabilityChange,
+}: SubscriptionPlansCardProps) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { status } = useStatus()
   const { status } = useStatus()
 
 
@@ -76,11 +79,11 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
   const [selectedPlan, setSelectedPlan] = useState<PlanRecord | null>(null)
   const [selectedPlan, setSelectedPlan] = useState<PlanRecord | null>(null)
 
 
   const enableStripe = !!status?.enable_stripe_topup
   const enableStripe = !!status?.enable_stripe_topup
-  const enableCreem = !!props.topupInfo?.enable_creem_topup
+  const enableCreem = !!topupInfo?.enable_creem_topup
   const enableOnlineTopUp = !!status?.enable_online_topup
   const enableOnlineTopUp = !!status?.enable_online_topup
   const epayMethods = useMemo(
   const epayMethods = useMemo(
-    () => getEpayMethods(props.topupInfo?.pay_methods),
-    [props.topupInfo?.pay_methods]
+    () => getEpayMethods(topupInfo?.pay_methods),
+    [topupInfo?.pay_methods]
   )
   )
 
 
   const fetchPlans = useCallback(async () => {
   const fetchPlans = useCallback(async () => {
@@ -148,6 +151,7 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
 
 
   const hasActive = activeSubscriptions.length > 0
   const hasActive = activeSubscriptions.length > 0
   const hasAny = allSubscriptions.length > 0
   const hasAny = allSubscriptions.length > 0
+  const isAvailable = loading || plans.length > 0 || hasAny
   const disablePref = !hasActive
   const disablePref = !hasActive
   const isSubPref =
   const isSubPref =
     billingPreference === 'subscription_first' ||
     billingPreference === 'subscription_first' ||
@@ -165,6 +169,10 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
     return map
     return map
   }, [allSubscriptions])
   }, [allSubscriptions])
 
 
+  useEffect(() => {
+    onAvailabilityChange?.(isAvailable)
+  }, [isAvailable, onAvailabilityChange])
+
   const planTitleMap = useMemo(() => {
   const planTitleMap = useMemo(() => {
     const map = new Map<number, string>()
     const map = new Map<number, string>()
     for (const p of plans) {
     for (const p of plans) {
@@ -191,11 +199,11 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
 
 
   if (loading) {
   if (loading) {
     return (
     return (
-      <Card className='overflow-hidden'>
-        <CardHeader className='border-b'>
+      <Card className='gap-0 overflow-hidden py-0'>
+        <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='h-6 w-32' />
         </CardHeader>
         </CardHeader>
-        <CardContent className='space-y-4 pt-6'>
+        <CardContent className='space-y-4 p-3 sm:p-5'>
           <Skeleton className='h-20 w-full' />
           <Skeleton className='h-20 w-full' />
           <div className='grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3'>
           <div className='grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3'>
             {Array.from({ length: 3 }).map((_, i) => (
             {Array.from({ length: 3 }).map((_, i) => (
@@ -213,27 +221,16 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
 
 
   return (
   return (
     <>
     <>
-      <Card className='overflow-hidden'>
-        <CardHeader className='border-b'>
-          <div className='flex items-center gap-3'>
-            <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
-              <Crown className='h-4 w-4' />
-            </div>
-            <div className='min-w-0'>
-              <CardTitle className='text-xl tracking-tight'>
-                {t('Subscription Plans')}
-              </CardTitle>
-              <CardDescription>
-                {t('Purchase a plan to enjoy model benefits')}
-              </CardDescription>
-            </div>
-          </div>
-        </CardHeader>
-        <CardContent className='space-y-5 pt-6'>
+      <TitledCard
+        title={t('Subscription Plans')}
+        description={t('Purchase a plan to enjoy model benefits')}
+        icon={<Crown className='h-4 w-4' />}
+        contentClassName='space-y-4 sm:space-y-5'
+      >
           {/* My subscriptions & billing preference */}
           {/* My subscriptions & billing preference */}
-          <div className='rounded-xl border p-4'>
-            <div className='flex flex-wrap items-center justify-between gap-3'>
-              <div className='flex items-center gap-2'>
+          <div className='rounded-xl border p-3 sm:p-4'>
+            <div className='flex flex-wrap items-center justify-between gap-2.5 sm:gap-3'>
+              <div className='flex min-w-0 flex-wrap items-center gap-2'>
                 <span className='text-sm font-medium'>
                 <span className='text-sm font-medium'>
                   {t('My Subscriptions')}
                   {t('My Subscriptions')}
                 </span>
                 </span>
@@ -265,12 +262,12 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
                   )}
                   )}
                 </span>
                 </span>
               </div>
               </div>
-              <div className='flex items-center gap-2'>
+              <div className='flex w-full items-center gap-2 sm:w-auto'>
                 <Select
                 <Select
                   value={displayPref}
                   value={displayPref}
                   onValueChange={handlePreferenceChange}
                   onValueChange={handlePreferenceChange}
                 >
                 >
-                  <SelectTrigger className='h-8 w-[140px] text-xs'>
+                  <SelectTrigger className='h-8 flex-1 text-xs sm:w-[140px] sm:flex-none'>
                     <SelectValue />
                     <SelectValue />
                   </SelectTrigger>
                   </SelectTrigger>
                   <SelectContent>
                   <SelectContent>
@@ -452,7 +449,7 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
 
 
           {/* Available plans grid */}
           {/* Available plans grid */}
           {plans.length > 0 ? (
           {plans.length > 0 ? (
-            <div className='grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3'>
+            <div className='grid grid-cols-1 gap-3 2xl:grid-cols-2 2xl:gap-4'>
               {plans.map((p, index) => {
               {plans.map((p, index) => {
                 const plan = p?.plan
                 const plan = p?.plan
                 if (!plan) return null
                 if (!plan) return null
@@ -485,7 +482,7 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
                       isPopular && 'border-primary/70 shadow-sm'
                       isPopular && 'border-primary/70 shadow-sm'
                     )}
                     )}
                   >
                   >
-                    <CardContent className='flex h-full flex-col p-4'>
+                    <CardContent className='flex h-full flex-col p-3.5 sm:p-4'>
                       <div className='mb-2 flex items-start justify-between gap-3'>
                       <div className='mb-2 flex items-start justify-between gap-3'>
                         <div className='min-w-0'>
                         <div className='min-w-0'>
                           <h4 className='truncate font-semibold'>
                           <h4 className='truncate font-semibold'>
@@ -568,8 +565,7 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
               {t('No plans available')}
               {t('No plans available')}
             </p>
             </p>
           )}
           )}
-        </CardContent>
-      </Card>
+      </TitledCard>
 
 
       <SubscriptionPurchaseDialog
       <SubscriptionPurchaseDialog
         open={purchaseOpen}
         open={purchaseOpen}

+ 5 - 5
web/default/src/features/wallet/components/wallet-stats-card.tsx

@@ -14,9 +14,9 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
   if (props.loading) {
   if (props.loading) {
     return (
     return (
       <div className='overflow-hidden rounded-lg border'>
       <div className='overflow-hidden rounded-lg border'>
-        <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
+        <div className='divide-border/60 grid grid-cols-3 divide-x'>
           {Array.from({ length: 3 }).map((_, i) => (
           {Array.from({ length: 3 }).map((_, i) => (
-            <div key={i} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+            <div key={i} className='px-3 py-3 sm:px-5 sm:py-4'>
               <Skeleton className='h-3.5 w-20' />
               <Skeleton className='h-3.5 w-20' />
               <Skeleton className='mt-2 h-7 w-28' />
               <Skeleton className='mt-2 h-7 w-28' />
               <Skeleton className='mt-1.5 h-3.5 w-24' />
               <Skeleton className='mt-1.5 h-3.5 w-24' />
@@ -50,9 +50,9 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
 
 
   return (
   return (
     <div className='overflow-hidden rounded-lg border'>
     <div className='overflow-hidden rounded-lg border'>
-      <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
+      <div className='divide-border/60 grid grid-cols-3 divide-x'>
         {stats.map((item) => (
         {stats.map((item) => (
-          <div key={item.label} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+          <div key={item.label} className='px-3 py-3 sm:px-5 sm:py-4'>
             <div className='flex items-center gap-2'>
             <div className='flex items-center gap-2'>
               <item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
               <item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
               <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
               <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
@@ -60,7 +60,7 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className='text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'>
+            <div className='text-foreground mt-1.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:mt-2 sm:text-2xl'>
               {item.value}
               {item.value}
             </div>
             </div>
             <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
             <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>

+ 28 - 13
web/default/src/features/wallet/index.tsx

@@ -54,6 +54,7 @@ export function Wallet(props: WalletProps) {
   const [creemDialogOpen, setCreemDialogOpen] = useState(false)
   const [creemDialogOpen, setCreemDialogOpen] = useState(false)
   const [selectedCreemProduct, setSelectedCreemProduct] =
   const [selectedCreemProduct, setSelectedCreemProduct] =
     useState<CreemProduct | null>(null)
     useState<CreemProduct | null>(null)
+  const [showSubscriptionPanel, setShowSubscriptionPanel] = useState(true)
 
 
   const { status } = useStatus()
   const { status } = useStatus()
   const { currency } = useSystemConfig()
   const { currency } = useSystemConfig()
@@ -231,6 +232,13 @@ export function Wallet(props: WalletProps) {
     return topupInfo?.discount?.[topupAmount] || DEFAULT_DISCOUNT_RATE
     return topupInfo?.discount?.[topupAmount] || DEFAULT_DISCOUNT_RATE
   }, [topupInfo, topupAmount])
   }, [topupInfo, topupAmount])
 
 
+  const handleSubscriptionAvailabilityChange = useCallback(
+    (available: boolean) => {
+      setShowSubscriptionPanel(available)
+    },
+    []
+  )
+
   return (
   return (
     <>
     <>
       <SectionPageLayout>
       <SectionPageLayout>
@@ -239,13 +247,17 @@ export function Wallet(props: WalletProps) {
           {t('Manage your balance and payment methods')}
           {t('Manage your balance and payment methods')}
         </SectionPageLayout.Description>
         </SectionPageLayout.Description>
         <SectionPageLayout.Content>
         <SectionPageLayout.Content>
-          <div className='mx-auto flex w-full max-w-7xl flex-col gap-4'>
+          <div className='mx-auto flex w-full max-w-7xl flex-col gap-4 sm:gap-5'>
             <WalletStatsCard user={user} loading={userLoading} />
             <WalletStatsCard user={user} loading={userLoading} />
 
 
-            <SubscriptionPlansCard topupInfo={topupInfo} />
-
-            <div className='grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(340px,0.4fr)] xl:items-start'>
-              <div className='min-w-0'>
+            <div
+              className={
+                showSubscriptionPanel
+                  ? 'grid gap-4 xl:grid-cols-[minmax(0,1.05fr)_minmax(360px,0.95fr)] xl:items-start'
+                  : 'grid gap-4'
+              }
+            >
+              <div id='wallet-add-funds' className='scroll-mt-4'>
                 <RechargeFormCard
                 <RechargeFormCard
                   topupInfo={topupInfo}
                   topupInfo={topupInfo}
                   presetAmounts={presetAmounts}
                   presetAmounts={presetAmounts}
@@ -279,15 +291,18 @@ export function Wallet(props: WalletProps) {
                 />
                 />
               </div>
               </div>
 
 
-              <div className='xl:sticky xl:top-6'>
-                <AffiliateRewardsCard
-                  user={user}
-                  affiliateLink={affiliateLink}
-                  onTransfer={() => setTransferDialogOpen(true)}
-                  loading={affiliateLoading}
-                />
-              </div>
+              <SubscriptionPlansCard
+                topupInfo={topupInfo}
+                onAvailabilityChange={handleSubscriptionAvailabilityChange}
+              />
             </div>
             </div>
+
+            <AffiliateRewardsCard
+              user={user}
+              affiliateLink={affiliateLink}
+              onTransfer={() => setTransferDialogOpen(true)}
+              loading={affiliateLoading}
+            />
           </div>
           </div>
         </SectionPageLayout.Content>
         </SectionPageLayout.Content>
       </SectionPageLayout>
       </SectionPageLayout>

+ 6 - 0
web/default/src/i18n/locales/en.json

@@ -1817,6 +1817,7 @@
     "Inter-group ratio overrides": "Inter-group ratio overrides",
     "Inter-group ratio overrides": "Inter-group ratio overrides",
     "Internal Notes": "Internal Notes",
     "Internal Notes": "Internal Notes",
     "Internal notes (not shown to users)": "Internal notes (not shown to users)",
     "Internal notes (not shown to users)": "Internal notes (not shown to users)",
+    "Interface Language": "Interface Language",
     "Internal Server Error!": "Internal Server Error!",
     "Internal Server Error!": "Internal Server Error!",
     "Invalid chat link. Please contact the administrator.": "Invalid chat link. Please contact the administrator.",
     "Invalid chat link. Please contact the administrator.": "Invalid chat link. Please contact the administrator.",
     "Invalid chat link. Please contact your administrator.": "Invalid chat link. Please contact your administrator.",
     "Invalid chat link. Please contact your administrator.": "Invalid chat link. Please contact your administrator.",
@@ -1893,6 +1894,9 @@
     "Kling": "Kling",
     "Kling": "Kling",
     "Knowledge Base ID *": "Knowledge Base ID *",
     "Knowledge Base ID *": "Knowledge Base ID *",
     "Landing page with system overview.": "Landing page with system overview.",
     "Landing page with system overview.": "Landing page with system overview.",
+    "Language Preferences": "Language Preferences",
+    "Language preference saved": "Language preference saved",
+    "Language preferences sync across your signed-in devices and affect API error messages.": "Language preferences sync across your signed-in devices and affect API error messages.",
     "Last check time": "Last check time",
     "Last check time": "Last check time",
     "Last detected addable models": "Last detected addable models",
     "Last detected addable models": "Last detected addable models",
     "Last Login": "Last Login",
     "Last Login": "Last Login",
@@ -3108,6 +3112,7 @@
     "Select items...": "Select items...",
     "Select items...": "Select items...",
     "Select key format": "Select key format",
     "Select key format": "Select key format",
     "Select Language": "Select Language",
     "Select Language": "Select Language",
+    "Select language": "Select language",
     "Select layout style": "Select layout style",
     "Select layout style": "Select layout style",
     "Select locations": "Select locations",
     "Select locations": "Select locations",
     "Select Model": "Select Model",
     "Select Model": "Select Model",
@@ -3171,6 +3176,7 @@
     "Set quota amount and limits": "Set quota amount and limits",
     "Set quota amount and limits": "Set quota amount and limits",
     "Set Request Header": "Set Request Header",
     "Set Request Header": "Set Request Header",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Set runtime request header: override entire value, or manipulate comma-separated tokens",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Set runtime request header: override entire value, or manipulate comma-separated tokens",
+    "Set the language used across the interface": "Set the language used across the interface",
     "Set Tag": "Set Tag",
     "Set Tag": "Set Tag",
     "Set tag for selected channels": "Set tag for selected channels",
     "Set tag for selected channels": "Set tag for selected channels",
     "Set the user's role (cannot be Root)": "Set the user's role (cannot be Root)",
     "Set the user's role (cannot be Root)": "Set the user's role (cannot be Root)",

+ 6 - 0
web/default/src/i18n/locales/fr.json

@@ -1817,6 +1817,7 @@
     "Inter-group ratio overrides": "Dérogations de ratio inter-groupes",
     "Inter-group ratio overrides": "Dérogations de ratio inter-groupes",
     "Internal Notes": "Notes internes",
     "Internal Notes": "Notes internes",
     "Internal notes (not shown to users)": "Notes internes (non visibles par les utilisateurs)",
     "Internal notes (not shown to users)": "Notes internes (non visibles par les utilisateurs)",
+    "Interface Language": "Langue de l'interface",
     "Internal Server Error!": "Erreur interne du serveur !",
     "Internal Server Error!": "Erreur interne du serveur !",
     "Invalid chat link. Please contact the administrator.": "Lien de chat invalide. Veuillez contacter l'administrateur.",
     "Invalid chat link. Please contact the administrator.": "Lien de chat invalide. Veuillez contacter l'administrateur.",
     "Invalid chat link. Please contact your administrator.": "Lien de chat invalide. Veuillez contacter votre administrateur.",
     "Invalid chat link. Please contact your administrator.": "Lien de chat invalide. Veuillez contacter votre administrateur.",
@@ -1893,6 +1894,9 @@
     "Kling": "Kling",
     "Kling": "Kling",
     "Knowledge Base ID *": "ID de la base de connaissances *",
     "Knowledge Base ID *": "ID de la base de connaissances *",
     "Landing page with system overview.": "Page d'accueil avec aperçu du système.",
     "Landing page with system overview.": "Page d'accueil avec aperçu du système.",
+    "Language Preferences": "Préférences de langue",
+    "Language preference saved": "Préférence de langue enregistrée",
+    "Language preferences sync across your signed-in devices and affect API error messages.": "Les préférences de langue se synchronisent sur vos appareils connectés et affectent les messages d'erreur de l'API.",
     "Last check time": "Dernière vérification",
     "Last check time": "Dernière vérification",
     "Last detected addable models": "Derniers modèles ajoutables détectés",
     "Last detected addable models": "Derniers modèles ajoutables détectés",
     "Last Login": "Dernière connexion",
     "Last Login": "Dernière connexion",
@@ -3108,6 +3112,7 @@
     "Select items...": "Sélectionner des éléments...",
     "Select items...": "Sélectionner des éléments...",
     "Select key format": "Sélectionner le format de clé",
     "Select key format": "Sélectionner le format de clé",
     "Select Language": "Sélectionner la langue",
     "Select Language": "Sélectionner la langue",
+    "Select language": "Sélectionner une langue",
     "Select layout style": "Sélectionner le style de mise en page",
     "Select layout style": "Sélectionner le style de mise en page",
     "Select locations": "Sélectionner des emplacements",
     "Select locations": "Sélectionner des emplacements",
     "Select Model": "Sélectionner le modèle",
     "Select Model": "Sélectionner le modèle",
@@ -3171,6 +3176,7 @@
     "Set quota amount and limits": "Définir le quota et les limites",
     "Set quota amount and limits": "Définir le quota et les limites",
     "Set Request Header": "Définir un en-tête de requête",
     "Set Request Header": "Définir un en-tête de requête",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Définir l'en-tête de requête : remplacer la valeur ou manipuler les tokens séparés par des virgules",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Définir l'en-tête de requête : remplacer la valeur ou manipuler les tokens séparés par des virgules",
+    "Set the language used across the interface": "Définir la langue utilisée dans l'interface",
     "Set Tag": "Définir un tag",
     "Set Tag": "Définir un tag",
     "Set tag for selected channels": "Définir un tag pour les canaux sélectionnés",
     "Set tag for selected channels": "Définir un tag pour les canaux sélectionnés",
     "Set the user's role (cannot be Root)": "Définir le rôle de l'utilisateur (ne peut pas être Root)",
     "Set the user's role (cannot be Root)": "Définir le rôle de l'utilisateur (ne peut pas être Root)",

+ 6 - 0
web/default/src/i18n/locales/ja.json

@@ -1817,6 +1817,7 @@
     "Inter-group ratio overrides": "グループ間比率上書き",
     "Inter-group ratio overrides": "グループ間比率上書き",
     "Internal Notes": "内部メモ",
     "Internal Notes": "内部メモ",
     "Internal notes (not shown to users)": ":内部メモ(ユーザーには表示されません)",
     "Internal notes (not shown to users)": ":内部メモ(ユーザーには表示されません)",
+    "Interface Language": "インターフェース言語",
     "Internal Server Error!": "内部サーバーエラー!",
     "Internal Server Error!": "内部サーバーエラー!",
     "Invalid chat link. Please contact the administrator.": "無効なチャットリンクです。管理者に連絡してください。",
     "Invalid chat link. Please contact the administrator.": "無効なチャットリンクです。管理者に連絡してください。",
     "Invalid chat link. Please contact your administrator.": "無効なチャットリンクです。管理者に連絡してください。",
     "Invalid chat link. Please contact your administrator.": "無効なチャットリンクです。管理者に連絡してください。",
@@ -1893,6 +1894,9 @@
     "Kling": "Kling",
     "Kling": "Kling",
     "Knowledge Base ID *": "ナレッジベースID *",
     "Knowledge Base ID *": "ナレッジベースID *",
     "Landing page with system overview.": "システム概要付きランディングページ。",
     "Landing page with system overview.": "システム概要付きランディングページ。",
+    "Language Preferences": "言語設定",
+    "Language preference saved": "言語設定を保存しました",
+    "Language preferences sync across your signed-in devices and affect API error messages.": "言語設定はログイン中のすべてのデバイスで同期され、API のエラーメッセージ言語にも反映されます。",
     "Last check time": "最終チェック時刻",
     "Last check time": "最終チェック時刻",
     "Last detected addable models": "最後に検出された追加可能モデル",
     "Last detected addable models": "最後に検出された追加可能モデル",
     "Last Login": "最終ログイン",
     "Last Login": "最終ログイン",
@@ -3108,6 +3112,7 @@
     "Select items...": "項目を選択...",
     "Select items...": "項目を選択...",
     "Select key format": "キーフォーマットを選択",
     "Select key format": "キーフォーマットを選択",
     "Select Language": "言語を選択",
     "Select Language": "言語を選択",
+    "Select language": "言語を選択",
     "Select layout style": "レイアウトスタイルを選択",
     "Select layout style": "レイアウトスタイルを選択",
     "Select locations": "ロケーションを選択",
     "Select locations": "ロケーションを選択",
     "Select Model": "モデルを選択",
     "Select Model": "モデルを選択",
@@ -3171,6 +3176,7 @@
     "Set quota amount and limits": "クォータ量と制限を設定",
     "Set quota amount and limits": "クォータ量と制限を設定",
     "Set Request Header": "リクエストヘッダーを設定",
     "Set Request Header": "リクエストヘッダーを設定",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "ランタイムリクエストヘッダーを設定:値全体を上書き、またはカンマ区切りトークンを操作",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "ランタイムリクエストヘッダーを設定:値全体を上書き、またはカンマ区切りトークンを操作",
+    "Set the language used across the interface": "インターフェースで使用する言語を設定します",
     "Set Tag": "タグを設定",
     "Set Tag": "タグを設定",
     "Set tag for selected channels": "選択したチャネルにタグを設定",
     "Set tag for selected channels": "選択したチャネルにタグを設定",
     "Set the user's role (cannot be Root)": "ユーザーのロールを設定します(Rootにはできません)",
     "Set the user's role (cannot be Root)": "ユーザーのロールを設定します(Rootにはできません)",

+ 6 - 0
web/default/src/i18n/locales/ru.json

@@ -1817,6 +1817,7 @@
     "Inter-group ratio overrides": "Переопределения соотношений между группами",
     "Inter-group ratio overrides": "Переопределения соотношений между группами",
     "Internal Notes": "Внутренние заметки",
     "Internal Notes": "Внутренние заметки",
     "Internal notes (not shown to users)": "Внутренние заметки (не показываются пользователям)",
     "Internal notes (not shown to users)": "Внутренние заметки (не показываются пользователям)",
+    "Interface Language": "Язык интерфейса",
     "Internal Server Error!": "Внутренняя ошибка сервера!",
     "Internal Server Error!": "Внутренняя ошибка сервера!",
     "Invalid chat link. Please contact the administrator.": "Неверная ссылка на чат. Пожалуйста, обратитесь к администратору.",
     "Invalid chat link. Please contact the administrator.": "Неверная ссылка на чат. Пожалуйста, обратитесь к администратору.",
     "Invalid chat link. Please contact your administrator.": "Недействительная ссылка чата. Обратитесь к администратору.",
     "Invalid chat link. Please contact your administrator.": "Недействительная ссылка чата. Обратитесь к администратору.",
@@ -1893,6 +1894,9 @@
     "Kling": "Kling",
     "Kling": "Kling",
     "Knowledge Base ID *": "ID базы знаний *",
     "Knowledge Base ID *": "ID базы знаний *",
     "Landing page with system overview.": "Главная страница с обзором системы.",
     "Landing page with system overview.": "Главная страница с обзором системы.",
+    "Language Preferences": "Языковые настройки",
+    "Language preference saved": "Языковая настройка сохранена",
+    "Language preferences sync across your signed-in devices and affect API error messages.": "Языковые настройки синхронизируются на всех ваших устройствах после входа и влияют на язык сообщений об ошибках API.",
     "Last check time": "Время последней проверки",
     "Last check time": "Время последней проверки",
     "Last detected addable models": "Последние обнаруженные модели для добавления",
     "Last detected addable models": "Последние обнаруженные модели для добавления",
     "Last Login": "Последний вход",
     "Last Login": "Последний вход",
@@ -3108,6 +3112,7 @@
     "Select items...": "Выберите элементы...",
     "Select items...": "Выберите элементы...",
     "Select key format": "Выберите формат ключа",
     "Select key format": "Выберите формат ключа",
     "Select Language": "Выбрать язык",
     "Select Language": "Выбрать язык",
+    "Select language": "Выберите язык",
     "Select layout style": "Выбрать стиль макета",
     "Select layout style": "Выбрать стиль макета",
     "Select locations": "Выбрать локации",
     "Select locations": "Выбрать локации",
     "Select Model": "Выбрать модель",
     "Select Model": "Выбрать модель",
@@ -3171,6 +3176,7 @@
     "Set quota amount and limits": "Настройте квоту и лимиты",
     "Set quota amount and limits": "Настройте квоту и лимиты",
     "Set Request Header": "Установить заголовок запроса",
     "Set Request Header": "Установить заголовок запроса",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Установить заголовок запроса: переопределить значение или управлять токенами через запятую",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Установить заголовок запроса: переопределить значение или управлять токенами через запятую",
+    "Set the language used across the interface": "Настроить язык интерфейса",
     "Set Tag": "Установить тег",
     "Set Tag": "Установить тег",
     "Set tag for selected channels": "Установить тег для выбранных каналов",
     "Set tag for selected channels": "Установить тег для выбранных каналов",
     "Set the user's role (cannot be Root)": "Установить роль пользователя (не может быть Root)",
     "Set the user's role (cannot be Root)": "Установить роль пользователя (не может быть Root)",

+ 6 - 0
web/default/src/i18n/locales/vi.json

@@ -1817,6 +1817,7 @@
     "Inter-group ratio overrides": "Tỷ lệ liên nhóm ghi đè",
     "Inter-group ratio overrides": "Tỷ lệ liên nhóm ghi đè",
     "Internal Notes": "Ghi chú nội bộ",
     "Internal Notes": "Ghi chú nội bộ",
     "Internal notes (not shown to users)": "Ghi chú nội bộ (không hiển thị cho người dùng)",
     "Internal notes (not shown to users)": "Ghi chú nội bộ (không hiển thị cho người dùng)",
+    "Interface Language": "Ngôn ngữ giao diện",
     "Internal Server Error!": "Lỗi máy chủ nội bộ!",
     "Internal Server Error!": "Lỗi máy chủ nội bộ!",
     "Invalid chat link. Please contact the administrator.": "Liên kết trò chuyện không hợp lệ. Vui lòng liên hệ quản trị viên.",
     "Invalid chat link. Please contact the administrator.": "Liên kết trò chuyện không hợp lệ. Vui lòng liên hệ quản trị viên.",
     "Invalid chat link. Please contact your administrator.": "Liên kết trò chuyện không hợp lệ. Vui lòng liên hệ với quản trị viên của bạn.",
     "Invalid chat link. Please contact your administrator.": "Liên kết trò chuyện không hợp lệ. Vui lòng liên hệ với quản trị viên của bạn.",
@@ -1893,6 +1894,9 @@
     "Kling": "Kling",
     "Kling": "Kling",
     "Knowledge Base ID *": "Mã số Cơ sở kiến thức *",
     "Knowledge Base ID *": "Mã số Cơ sở kiến thức *",
     "Landing page with system overview.": "Trang chủ với tổng quan hệ thống.",
     "Landing page with system overview.": "Trang chủ với tổng quan hệ thống.",
+    "Language Preferences": "Tùy chọn ngôn ngữ",
+    "Language preference saved": "Đã lưu tùy chọn ngôn ngữ",
+    "Language preferences sync across your signed-in devices and affect API error messages.": "Tùy chọn ngôn ngữ sẽ đồng bộ trên các thiết bị đã đăng nhập và ảnh hưởng đến ngôn ngữ thông báo lỗi API.",
     "Last check time": "Thời gian kiểm tra gần nhất",
     "Last check time": "Thời gian kiểm tra gần nhất",
     "Last detected addable models": "Mô hình có thể thêm được phát hiện gần nhất",
     "Last detected addable models": "Mô hình có thể thêm được phát hiện gần nhất",
     "Last Login": "Lần đăng nhập cuối",
     "Last Login": "Lần đăng nhập cuối",
@@ -3108,6 +3112,7 @@
     "Select items...": "Chọn các mục...",
     "Select items...": "Chọn các mục...",
     "Select key format": "Chọn định dạng khóa",
     "Select key format": "Chọn định dạng khóa",
     "Select Language": "Chọn Ngôn ngữ",
     "Select Language": "Chọn Ngôn ngữ",
+    "Select language": "Chọn ngôn ngữ",
     "Select layout style": "Chọn kiểu bố cục",
     "Select layout style": "Chọn kiểu bố cục",
     "Select locations": "Chọn vị trí",
     "Select locations": "Chọn vị trí",
     "Select Model": "Chọn mẫu",
     "Select Model": "Chọn mẫu",
@@ -3171,6 +3176,7 @@
     "Set quota amount and limits": "Thiết lập hạn mức và giới hạn",
     "Set quota amount and limits": "Thiết lập hạn mức và giới hạn",
     "Set Request Header": "Đặt header yêu cầu",
     "Set Request Header": "Đặt header yêu cầu",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Đặt header yêu cầu runtime: ghi đè toàn bộ giá trị hoặc thao tác token phân cách bằng dấu phẩy",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "Đặt header yêu cầu runtime: ghi đè toàn bộ giá trị hoặc thao tác token phân cách bằng dấu phẩy",
+    "Set the language used across the interface": "Đặt ngôn ngữ sử dụng trong giao diện",
     "Set Tag": "Gán Thẻ",
     "Set Tag": "Gán Thẻ",
     "Set tag for selected channels": "Đặt thẻ cho các kênh đã chọn",
     "Set tag for selected channels": "Đặt thẻ cho các kênh đã chọn",
     "Set the user's role (cannot be Root)": "Đặt vai trò của người dùng (không được là Root)",
     "Set the user's role (cannot be Root)": "Đặt vai trò của người dùng (không được là Root)",

+ 6 - 0
web/default/src/i18n/locales/zh.json

@@ -1817,6 +1817,7 @@
     "Inter-group ratio overrides": "分组间比例覆盖",
     "Inter-group ratio overrides": "分组间比例覆盖",
     "Internal Notes": "内部备注",
     "Internal Notes": "内部备注",
     "Internal notes (not shown to users)": "内部备注(不显示给用户)",
     "Internal notes (not shown to users)": "内部备注(不显示给用户)",
+    "Interface Language": "界面语言",
     "Internal Server Error!": "内部服务器错误!",
     "Internal Server Error!": "内部服务器错误!",
     "Invalid chat link. Please contact the administrator.": "无效的聊天链接。请联系管理员。",
     "Invalid chat link. Please contact the administrator.": "无效的聊天链接。请联系管理员。",
     "Invalid chat link. Please contact your administrator.": "无效的聊天链接。请联系您的管理员。",
     "Invalid chat link. Please contact your administrator.": "无效的聊天链接。请联系您的管理员。",
@@ -1893,6 +1894,9 @@
     "Kling": "Kling",
     "Kling": "Kling",
     "Knowledge Base ID *": "知识库 ID *",
     "Knowledge Base ID *": "知识库 ID *",
     "Landing page with system overview.": "带有系统概览的登陆页面。",
     "Landing page with system overview.": "带有系统概览的登陆页面。",
+    "Language Preferences": "语言偏好",
+    "Language preference saved": "语言偏好已保存",
+    "Language preferences sync across your signed-in devices and affect API error messages.": "语言偏好会同步到您登录的所有设备,并影响 API 错误消息语言。",
     "Last check time": "上次检测时间",
     "Last check time": "上次检测时间",
     "Last detected addable models": "上次检测到可加入模型",
     "Last detected addable models": "上次检测到可加入模型",
     "Last Login": "最后登录",
     "Last Login": "最后登录",
@@ -3108,6 +3112,7 @@
     "Select items...": "选择项目...",
     "Select items...": "选择项目...",
     "Select key format": "请选择密钥格式",
     "Select key format": "请选择密钥格式",
     "Select Language": "选择语言",
     "Select Language": "选择语言",
+    "Select language": "选择语言",
     "Select layout style": "选择布局样式",
     "Select layout style": "选择布局样式",
     "Select locations": "选择位置",
     "Select locations": "选择位置",
     "Select Model": "选择模型",
     "Select Model": "选择模型",
@@ -3171,6 +3176,7 @@
     "Set quota amount and limits": "设置令牌可用额度和数量",
     "Set quota amount and limits": "设置令牌可用额度和数量",
     "Set Request Header": "设置请求头",
     "Set Request Header": "设置请求头",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "设置运行期请求头:可直接覆盖整条值,也可对逗号分隔的 token 做处理",
     "Set runtime request header: override entire value, or manipulate comma-separated tokens": "设置运行期请求头:可直接覆盖整条值,也可对逗号分隔的 token 做处理",
+    "Set the language used across the interface": "设置界面显示语言",
     "Set Tag": "设置标签",
     "Set Tag": "设置标签",
     "Set tag for selected channels": "为选定的渠道设置标签",
     "Set tag for selected channels": "为选定的渠道设置标签",
     "Set the user's role (cannot be Root)": "设置用户角色(不能是 Root)",
     "Set the user's role (cannot be Root)": "设置用户角色(不能是 Root)",

+ 14 - 0
web/default/src/lib/time.ts

@@ -65,6 +65,20 @@ export function getNormalizedDateRange(
   }
   }
 }
 }
 
 
+/**
+ * Calculate a rolling date range ending at the current moment.
+ * Example: 1 day means the last 24 hours, not yesterday 00:00 to today 23:59.
+ */
+export function getRollingDateRange(
+  days: number,
+  fromDate: Date = new Date()
+): { start: Date; end: Date } {
+  const end = new Date(fromDate)
+  const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000)
+
+  return { start, end }
+}
+
 /**
 /**
  * Compute time range as Unix timestamps (seconds)
  * Compute time range as Unix timestamps (seconds)
  * @param days Default number of days if no dates provided
  * @param days Default number of days if no dates provided

+ 1 - 1
web/default/src/stores/auth-store.ts

@@ -26,7 +26,7 @@ export interface AuthUser {
   wechat_id?: string
   wechat_id?: string
   telegram_id?: string
   telegram_id?: string
   linux_do_id?: string
   linux_do_id?: string
-  setting?: Record<string, unknown>
+  setting?: Record<string, unknown> | string
   stripe_customer?: string
   stripe_customer?: string
   sidebar_modules?: string
   sidebar_modules?: string
   permissions?: UserPermissions
   permissions?: UserPermissions