fix(ui): make admin lists usable on mobile viewports

This commit is contained in:
Steven RYDELL
2026-07-30 19:54:34 +02:00
parent d1284de415
commit 1ab574a42a
6 changed files with 49 additions and 34 deletions
+3 -3
View File
@@ -408,9 +408,9 @@ function WizardShell({ children }: { children: React.ReactNode }) {
<header className="flex items-center px-6 py-4 border-b bg-background">
<DefaultLogo />
</header>
<ScrollArea role="main" className="flex-1">
<div className="p-6">
<div className="mx-auto max-w-3xl">{children}</div>
<ScrollArea role="main" className="min-w-0 flex-1">
<div className="min-w-0 p-4 sm:p-6">
<div className="mx-auto w-full min-w-0 max-w-3xl">{children}</div>
</div>
</ScrollArea>
</div>
+3 -3
View File
@@ -743,7 +743,7 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
const sectionsToRender = buildSections(combinedForm, currentFields, isCreate, edition);
return (
<div className="mx-auto max-w-4xl space-y-6">
<div className="mx-auto w-full min-w-0 max-w-4xl space-y-6">
<div className="flex items-center gap-4">
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ArrowLeft className="h-5 w-5" />
@@ -845,7 +845,7 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
</Card>
))}
<div className="flex items-center justify-between pt-2 pb-8">
<div className="flex flex-col-reverse gap-3 pt-2 pb-8 sm:flex-row sm:items-center sm:justify-between">
<div>
{canDelete && (
<Button type="button" variant="destructive" disabled={saving} onClick={() => setDeleteConfirmOpen(true)}>
@@ -854,7 +854,7 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
</Button>
)}
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center justify-end gap-3">
{isDirty && (
<span className="text-xs text-muted-foreground">{t('form.unsavedChangesLabel', 'Unsaved changes')}</span>
)}
+26 -19
View File
@@ -1392,13 +1392,13 @@ export function DynamicList({ viewName }: DynamicListProps) {
}
return (
<div className="relative space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight">{list.title}</h1>
{list.subtitle && <p className="text-sm text-muted-foreground mt-1">{list.subtitle}</p>}
<div className="relative min-w-0 space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<h1 className="text-2xl font-bold tracking-tight truncate">{list.title}</h1>
{list.subtitle && <p className="text-sm text-muted-foreground mt-1 line-clamp-2">{list.subtitle}</p>}
</div>
<div className="flex items-center gap-2">
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
{hasMassActions && selectedIds.size > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -1559,16 +1559,18 @@ export function DynamicList({ viewName }: DynamicListProps) {
</Card>
)}
<div className="rounded-lg border bg-background shadow-sm">
<div className="min-w-0 rounded-lg border bg-background shadow-sm">
{/* The scroll container must clip with the parent's inner radius
(outer radius minus the 1px border), otherwise filled header rows
paint square corners behind the rounded border. */}
<div className="overflow-x-auto rounded-[calc(var(--radius-lg)-1px)]">
<table className="w-full text-sm">
paint square corners behind the rounded border.
`w-max min-w-full` keeps the table at least as wide as the card,
but lets wide column sets scroll horizontally inside this wrapper. */}
<div className="overflow-x-auto overscroll-x-contain rounded-[calc(var(--radius-lg)-1px)] [-webkit-overflow-scrolling:touch]">
<table className="w-max min-w-full text-sm">
<thead>
<tr className="border-b bg-muted">
{hasMassActions && (
<th className="w-10 px-3 py-3">
<th className="w-10 px-3 py-3 whitespace-nowrap">
<Checkbox
checked={items.length > 0 && selectedIds.size === items.length}
onCheckedChange={toggleSelectAll}
@@ -1577,7 +1579,10 @@ export function DynamicList({ viewName }: DynamicListProps) {
</th>
)}
{displayColumns.map((col) => (
<th key={col.name} className="px-3 py-3 text-left font-medium text-muted-foreground">
<th
key={col.name}
className="px-3 py-3 text-left font-medium text-muted-foreground whitespace-nowrap"
>
<div className="flex items-center">
{col.label}
{renderSortIndicator(col.name)}
@@ -1585,7 +1590,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
</th>
))}
{hasItemActions && (
<th className="w-12 px-3 py-3 text-right font-medium text-muted-foreground">
<th className="w-12 px-3 py-3 text-right font-medium text-muted-foreground whitespace-nowrap">
{t('list.actions', 'Actions')}
</th>
)}
@@ -1620,7 +1625,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
onClick={() => handleRowClick(item)}
>
{hasMassActions && (
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
<td className="px-3 py-2 whitespace-nowrap" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={selectedIds.has(itemId)}
onCheckedChange={() => toggleSelectItem(itemId)}
@@ -1629,7 +1634,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
</td>
)}
{displayColumns.map((col) => (
<td key={col.name} className="px-3 py-2">
<td key={col.name} className="px-3 py-2 whitespace-nowrap">
{isWebApplications && col.name === 'enabled' && !fields[col.name] ? (
item.enabled === true ? (
<Check className="h-4 w-4 text-green-600" />
@@ -1664,7 +1669,9 @@ export function DynamicList({ viewName }: DynamicListProps) {
)}
</td>
))}
{hasItemActions && <td className="px-3 py-2 text-right">{renderItemActions(item)}</td>}
{hasItemActions && (
<td className="px-3 py-2 text-right whitespace-nowrap">{renderItemActions(item)}</td>
)}
</tr>
);
})
@@ -1675,8 +1682,8 @@ export function DynamicList({ viewName }: DynamicListProps) {
</div>
{items.length > 0 && (
<div className="flex items-center justify-between text-sm text-muted-foreground">
<div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between text-sm text-muted-foreground">
<div className="min-w-0">
{total !== null
? t('list.showing', 'Showing {{from}}-{{to}} of {{total}} {{name}}', {
from: rangeStart,
@@ -1688,7 +1695,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
count: items.length,
})}
</div>
<div className="flex items-center gap-2">
<div className="flex shrink-0 items-center gap-2 self-end sm:self-auto">
<Button variant="outline" size="sm" disabled={!hasPrevPage || loading} onClick={handlePrevPage}>
{t('list.previous', 'Previous')}
</Button>
+10 -2
View File
@@ -18,8 +18,16 @@ const ScrollArea = React.forwardRef<
viewportClassName?: string;
}
>(({ className, viewportClassName, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Viewport className={cn('h-full w-full rounded-[inherit]', viewportClassName)}>
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative min-w-0 overflow-hidden', className)} {...props}>
{/*
Radix wraps children in a `display: table` div with an intrinsic min-width.
That lets wide tables expand the whole shell and clip under overflow-x:hidden,
so horizontal scroll never reaches the table's own overflow-x-auto wrapper.
Force the inner wrapper to shrink to the viewport width instead.
*/}
<ScrollAreaPrimitive.Viewport
className={cn('h-full w-full rounded-[inherit] [&>div]:!block [&>div]:!min-w-0', viewportClassName)}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
+3 -3
View File
@@ -126,8 +126,8 @@ function ViewField({ label, field, value, schema }: { label: string; field: Fiel
const isBlock = isBlockType(field.type, value);
return (
<div className={isBlock ? 'space-y-1' : 'flex items-baseline gap-2'}>
<dt className="flex items-center gap-1 text-sm text-muted-foreground shrink-0 min-w-[140px]">
<div className={isBlock ? 'space-y-1' : 'flex flex-col gap-1 sm:flex-row sm:items-baseline sm:gap-2'}>
<dt className="flex items-center gap-1 text-sm text-muted-foreground shrink-0 sm:min-w-[140px]">
{label}
{field.description && (
<TooltipProvider>
@@ -144,7 +144,7 @@ function ViewField({ label, field, value, schema }: { label: string; field: Fiel
</TooltipProvider>
)}
</dt>
<dd className="text-sm min-w-0">
<dd className="text-sm min-w-0 break-words">
<ViewValue type={field.type} value={value} schema={schema} />
</dd>
</div>
+4 -4
View File
@@ -315,14 +315,14 @@ export default function AdminPanel() {
return (
<div className="flex h-screen flex-col">
<TopBar />
<div className="flex min-h-0 flex-1">
<div className="flex min-h-0 min-w-0 flex-1">
<Sidebar />
<ScrollArea
role="main"
className={`flex-1 bg-content-background transition-[margin] ${sidebarOpen ? 'md:ml-64' : ''}`}
className={`min-w-0 flex-1 bg-content-background transition-[margin] ${sidebarOpen ? 'md:ml-64' : ''}`}
>
<div className="p-6">
<div className="mx-auto w-full max-w-7xl">
<div className="min-w-0 p-4 sm:p-6">
<div className="mx-auto w-full min-w-0 max-w-7xl">
{/* Keyed on the active account: forces MainContent (and every
view it renders) to fully remount on switch, so
account-scoped views can't keep showing stale data fetched