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">
|
<header className="flex items-center px-6 py-4 border-b bg-background">
|
||||||
<DefaultLogo />
|
<DefaultLogo />
|
||||||
</header>
|
</header>
|
||||||
<ScrollArea role="main" className="flex-1">
|
<ScrollArea role="main" className="min-w-0 flex-1">
|
||||||
<div className="p-6">
|
<div className="min-w-0 p-4 sm:p-6">
|
||||||
<div className="mx-auto max-w-3xl">{children}</div>
|
<div className="mx-auto w-full min-w-0 max-w-3xl">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -743,7 +743,7 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
|
|||||||
const sectionsToRender = buildSections(combinedForm, currentFields, isCreate, edition);
|
const sectionsToRender = buildSections(combinedForm, currentFields, isCreate, edition);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-4">
|
||||||
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
@@ -845,7 +845,7 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
|
|||||||
</Card>
|
</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>
|
<div>
|
||||||
{canDelete && (
|
{canDelete && (
|
||||||
<Button type="button" variant="destructive" disabled={saving} onClick={() => setDeleteConfirmOpen(true)}>
|
<Button type="button" variant="destructive" disabled={saving} onClick={() => setDeleteConfirmOpen(true)}>
|
||||||
@@ -854,7 +854,7 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-center justify-end gap-3">
|
||||||
{isDirty && (
|
{isDirty && (
|
||||||
<span className="text-xs text-muted-foreground">{t('form.unsavedChangesLabel', 'Unsaved changes')}</span>
|
<span className="text-xs text-muted-foreground">{t('form.unsavedChangesLabel', 'Unsaved changes')}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1392,13 +1392,13 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative space-y-4">
|
<div className="relative min-w-0 space-y-4">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">{list.title}</h1>
|
<h1 className="text-2xl font-bold tracking-tight truncate">{list.title}</h1>
|
||||||
{list.subtitle && <p className="text-sm text-muted-foreground mt-1">{list.subtitle}</p>}
|
{list.subtitle && <p className="text-sm text-muted-foreground mt-1 line-clamp-2">{list.subtitle}</p>}
|
||||||
</div>
|
</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 && (
|
{hasMassActions && selectedIds.size > 0 && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -1559,16 +1559,18 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
</Card>
|
</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
|
{/* The scroll container must clip with the parent's inner radius
|
||||||
(outer radius minus the 1px border), otherwise filled header rows
|
(outer radius minus the 1px border), otherwise filled header rows
|
||||||
paint square corners behind the rounded border. */}
|
paint square corners behind the rounded border.
|
||||||
<div className="overflow-x-auto rounded-[calc(var(--radius-lg)-1px)]">
|
`w-max min-w-full` keeps the table at least as wide as the card,
|
||||||
<table className="w-full text-sm">
|
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>
|
<thead>
|
||||||
<tr className="border-b bg-muted">
|
<tr className="border-b bg-muted">
|
||||||
{hasMassActions && (
|
{hasMassActions && (
|
||||||
<th className="w-10 px-3 py-3">
|
<th className="w-10 px-3 py-3 whitespace-nowrap">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={items.length > 0 && selectedIds.size === items.length}
|
checked={items.length > 0 && selectedIds.size === items.length}
|
||||||
onCheckedChange={toggleSelectAll}
|
onCheckedChange={toggleSelectAll}
|
||||||
@@ -1577,7 +1579,10 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{displayColumns.map((col) => (
|
{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">
|
<div className="flex items-center">
|
||||||
{col.label}
|
{col.label}
|
||||||
{renderSortIndicator(col.name)}
|
{renderSortIndicator(col.name)}
|
||||||
@@ -1585,7 +1590,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{hasItemActions && (
|
{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')}
|
{t('list.actions', 'Actions')}
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
@@ -1620,7 +1625,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
onClick={() => handleRowClick(item)}
|
onClick={() => handleRowClick(item)}
|
||||||
>
|
>
|
||||||
{hasMassActions && (
|
{hasMassActions && (
|
||||||
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
|
<td className="px-3 py-2 whitespace-nowrap" onClick={(e) => e.stopPropagation()}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedIds.has(itemId)}
|
checked={selectedIds.has(itemId)}
|
||||||
onCheckedChange={() => toggleSelectItem(itemId)}
|
onCheckedChange={() => toggleSelectItem(itemId)}
|
||||||
@@ -1629,7 +1634,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{displayColumns.map((col) => (
|
{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] ? (
|
{isWebApplications && col.name === 'enabled' && !fields[col.name] ? (
|
||||||
item.enabled === true ? (
|
item.enabled === true ? (
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
@@ -1664,7 +1669,9 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -1675,8 +1682,8 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between text-sm text-muted-foreground">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
{total !== null
|
{total !== null
|
||||||
? t('list.showing', 'Showing {{from}}-{{to}} of {{total}} {{name}}', {
|
? t('list.showing', 'Showing {{from}}-{{to}} of {{total}} {{name}}', {
|
||||||
from: rangeStart,
|
from: rangeStart,
|
||||||
@@ -1688,7 +1695,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
count: items.length,
|
count: items.length,
|
||||||
})}
|
})}
|
||||||
</div>
|
</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}>
|
<Button variant="outline" size="sm" disabled={!hasPrevPage || loading} onClick={handlePrevPage}>
|
||||||
{t('list.previous', 'Previous')}
|
{t('list.previous', 'Previous')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,8 +18,16 @@ const ScrollArea = React.forwardRef<
|
|||||||
viewportClassName?: string;
|
viewportClassName?: string;
|
||||||
}
|
}
|
||||||
>(({ className, viewportClassName, children, ...props }, ref) => (
|
>(({ className, viewportClassName, children, ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
|
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative min-w-0 overflow-hidden', className)} {...props}>
|
||||||
<ScrollAreaPrimitive.Viewport className={cn('h-full w-full rounded-[inherit]', viewportClassName)}>
|
{/*
|
||||||
|
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}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ function ViewField({ label, field, value, schema }: { label: string; field: Fiel
|
|||||||
const isBlock = isBlockType(field.type, value);
|
const isBlock = isBlockType(field.type, value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isBlock ? 'space-y-1' : 'flex items-baseline gap-2'}>
|
<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 min-w-[140px]">
|
<dt className="flex items-center gap-1 text-sm text-muted-foreground shrink-0 sm:min-w-[140px]">
|
||||||
{label}
|
{label}
|
||||||
{field.description && (
|
{field.description && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -144,7 +144,7 @@ function ViewField({ label, field, value, schema }: { label: string; field: Fiel
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</dt>
|
</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} />
|
<ViewValue type={field.type} value={value} schema={schema} />
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -315,14 +315,14 @@ export default function AdminPanel() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<div className="flex min-h-0 flex-1">
|
<div className="flex min-h-0 min-w-0 flex-1">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
role="main"
|
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="min-w-0 p-4 sm:p-6">
|
||||||
<div className="mx-auto w-full max-w-7xl">
|
<div className="mx-auto w-full min-w-0 max-w-7xl">
|
||||||
{/* Keyed on the active account: forces MainContent (and every
|
{/* Keyed on the active account: forces MainContent (and every
|
||||||
view it renders) to fully remount on switch, so
|
view it renders) to fully remount on switch, so
|
||||||
account-scoped views can't keep showing stale data fetched
|
account-scoped views can't keep showing stale data fetched
|
||||||
|
|||||||
Reference in New Issue
Block a user