feat(ui): improve mobile responsive layouts
This commit is contained in:
@@ -62,7 +62,7 @@ function ListSkeleton() {
|
||||
<Skeleton className='h-4 w-32' />
|
||||
<Skeleton className='h-5 w-16 rounded-full' />
|
||||
</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'>
|
||||
<Skeleton className='mb-1 h-2 w-8' />
|
||||
<Skeleton className='h-4 w-full' />
|
||||
@@ -136,9 +136,9 @@ function CompactRow<TData>({ row }: { row: Row<TData> }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2: Key fields side by side */}
|
||||
{/* Row 2: Key fields wrap into compact columns instead of squeezing */}
|
||||
{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) => {
|
||||
const label = getCellLabel(cell)
|
||||
return (
|
||||
@@ -260,7 +260,7 @@ export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return (
|
||||
<div className='rounded-lg border p-8'>
|
||||
<div className='rounded-lg border p-6'>
|
||||
<Empty className='border-none p-0'>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant='icon'>
|
||||
|
||||
+5
-5
@@ -32,12 +32,12 @@ export function DataTablePagination<TData>({
|
||||
<div
|
||||
className={cn(
|
||||
'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 }}
|
||||
>
|
||||
<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}}', {
|
||||
current: currentPage,
|
||||
total: totalPages,
|
||||
@@ -50,7 +50,7 @@ export function DataTablePagination<TData>({
|
||||
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} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side='top'>
|
||||
@@ -74,7 +74,7 @@ export function DataTablePagination<TData>({
|
||||
total: totalPages,
|
||||
})}
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className='flex items-center space-x-1.5 sm:space-x-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='size-8 p-0 @max-md/content:hidden'
|
||||
|
||||
+5
-5
@@ -64,14 +64,14 @@ export function DataTableToolbar<TData>({
|
||||
onChange={(event) =>
|
||||
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
|
||||
placeholder={resolvedSearchPlaceholder}
|
||||
value={table.getState().globalFilter ?? ''}
|
||||
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 (
|
||||
<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 */}
|
||||
{customSearch !== undefined ? customSearch : searchInput}
|
||||
|
||||
@@ -122,7 +122,7 @@ export function DataTableToolbar<TData>({
|
||||
<Button
|
||||
variant='outline'
|
||||
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)}
|
||||
>
|
||||
<SlidersHorizontal className='h-3.5 w-3.5' />
|
||||
@@ -142,7 +142,7 @@ export function DataTableToolbar<TData>({
|
||||
|
||||
{/* Mobile: collapsible filter area */}
|
||||
{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>}
|
||||
{filterChips}
|
||||
{resetButton}
|
||||
|
||||
+2
-2
@@ -25,10 +25,10 @@ export function DataTableViewOptions<TData>({
|
||||
<Button
|
||||
variant='outline'
|
||||
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' />
|
||||
{t('View')}
|
||||
<span className='hidden sm:inline'>{t('View')}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-[150px]'>
|
||||
|
||||
@@ -70,15 +70,15 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
|
||||
<AppHeader />
|
||||
|
||||
<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'>
|
||||
<h2 className='text-base font-bold tracking-tight sm:text-lg'>
|
||||
<h2 className='truncate text-base font-bold tracking-tight sm:text-lg'>
|
||||
{title}
|
||||
</h2>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@@ -91,11 +91,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
|
||||
</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
|
||||
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>
|
||||
</PageFooterProvider>
|
||||
|
||||
+81
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user