fix(ui): make admin lists usable on mobile viewports
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user