fix(mobile): audit and fix layout overflow across dashboard pages at 375/768px

Audited /dashboard, /dashboard/sites (+ detail), /dashboard/workforce/workers,
/dashboard/workforce/on-site, /dashboard/plant/vehicles (+ detail),
/dashboard/plant/equipment (+ detail), /dashboard/plant/fuel-logs,
/dashboard/financial/purchase-orders (+ detail),
/dashboard/financial/delivery-notes/inbox, /dashboard/financial/suppliers,
and /dashboard/chat at 375x812 and 768x1024 using a scroll-right test to
catch real horizontal page overflow (as opposed to a table's own,
intentional internal scroll).

Two repeating bug patterns accounted for every overflow found:

1. A page-specific `right={<div className="flex items-center gap-2">...}`
   passed to Topbar can carry two or three labelled buttons (edit/import/
   merge/back etc.). Without `flex-wrap` they don't fit a 375px screen in
   one row and push the whole page sideways. Fixed on the vehicles,
   equipment, and site detail pages, plus their list-page counterparts
   (vehicles fuel-view, equipment plant-view) — all five instances of
   this exact pattern. Same fix applied to a delivery-note card's button
   cluster on the PO detail page, and to Topbar's own right-side
   container as a general backstop.

2. `Intl.NumberFormat("en-ZA", ...)` joins currency/number groups with
   non-breaking spaces, so a wide value like "R 24 988 663" can't wrap.
   In a 3-up grid (site cards' Contract/Spent/Remaining) that overflowed
   the column; fixed by stacking those to 1-up below `sm`. In a 2-up grid
   (PO detail KPI tiles) added `min-w-0` so the tile clips instead of
   growing the grid track past the viewport.

Also: the shared Dialog now caps at `max-h-[90dvh]` with internal
scroll (a dialog taller than the phone viewport had no way to reach
its close button or footer before), and the sidebar hamburger button
was overlapping the Topbar's title on every page below `lg` — reserved
space for it instead.

Tables already scroll horizontally inside their own card via the
shared `<Table>` component or an `overflow-auto` wrapper, and KPI
grids already collapse responsively — both were fine everywhere checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-09-27 09:44:19 +02:00
parent 7009197716
commit 3feddebcaa
9 changed files with 44 additions and 12 deletions

View file

@ -124,7 +124,11 @@ function amberTile(
? "text-blue-600 dark:text-blue-400"
: "text-[#F2B165]";
return (
<Card className="relative overflow-hidden border-border/60">
// `min-w-0`: without it, a grid item won't shrink below its content's
// intrinsic width — a wide, non-breaking-space-grouped number (e.g.
// "4 777,92 u") then grows the grid track past the column's share,
// pushing the whole page wider than the viewport at 2-up.
<Card className="relative min-w-0 overflow-hidden border-border/60">
<span
aria-hidden
className={`absolute inset-y-0 left-0 w-1 bg-gradient-to-b ${strip}`}
@ -857,7 +861,9 @@ function DeliveryNoteCard({
</Badge>
</div>
</div>
<div className="flex items-center gap-2">
{/* flex-wrap: up to three buttons (review link, view slip,
verify) here would overflow a 375px screen in one row. */}
<div className="flex flex-wrap items-center gap-2">
{dn.batchNoteId && (
<Button asChild variant="outline" size="sm">
<Link href={`/dashboard/financial/delivery-notes/document/${dn.batchNoteId}`}>

View file

@ -134,7 +134,8 @@ export default async function PlantDetailPage({
title={vehicle.description}
subtitle={`${vehicleProjectCode} · ${vehicle.fuel_limits || "No fuel limit set"}`}
right={
<div className="flex items-center gap-2">
// flex-wrap: two labelled buttons here would overflow a 375px screen.
<div className="flex flex-wrap items-center gap-2">
<MergeVehicleButton
vehicleId={vehicleId}
vehicleLabel={vehicle.description}

View file

@ -264,7 +264,9 @@ export function PlantView({
title={title}
subtitle={subtitle}
right={
<div className="flex items-center gap-2">
// flex-wrap: three items here would overflow a 375px screen
// in one row.
<div className="flex flex-wrap items-center gap-2">
<a
href="/api/plant-hours/template"
className="inline-flex h-9 items-center gap-1 rounded-md border border-border bg-card px-3 text-sm hover:bg-muted"

View file

@ -179,7 +179,8 @@ export default async function VehicleDetailPage({
title={vehicle.description}
subtitle={`${vehicleProjectCode} · ${limitStr || "No fuel limit set"}`}
right={
<div className="flex items-center gap-2">
// flex-wrap: two labelled buttons here would overflow a 375px screen.
<div className="flex flex-wrap items-center gap-2">
<MergeVehicleButton
vehicleId={vehicleId}
vehicleLabel={vehicle.description}

View file

@ -330,7 +330,10 @@ export function FuelView({
title={title}
subtitle={subtitle}
right={
<div className="flex items-center gap-2">
// flex-wrap: two labelled buttons here (plus the scope switcher
// and theme toggle already in Topbar) are wider than a 375px
// screen; without wrapping this pushed the whole page sideways.
<div className="flex flex-wrap items-center gap-2">
<EditModeToggle
editing={edit.editing}
onToggle={handleToggle}

View file

@ -259,7 +259,8 @@ export default async function SiteDetailPage({
title={site.name}
subtitle={`${site.project_code} · ${site.duration || "—"}`}
right={
<div className="flex items-center gap-2">
// flex-wrap: two labelled buttons here would overflow a 375px screen.
<div className="flex flex-wrap items-center gap-2">
<EditSiteButton
site={{
id: site.id,

View file

@ -179,8 +179,13 @@ function SiteCard({ site }: { site: SiteSummary }) {
</div>
</CardHeader>
<CardContent className="space-y-4 pl-5">
{/* Contract + burn */}
<div className="grid grid-cols-3 gap-3">
{/* Contract + burn. `Intl.NumberFormat` inserts non-breaking
spaces in these currency strings (so "R 24 988 663" never
splits mid-amount), which means a wide value can't wrap onto
a second line — at 3-up it just overflows into the next
column. Stack to 1-up below `sm` where there's no room for
three currency columns; 3-up returns once there's space. */}
<div className="grid grid-cols-1 gap-1 sm:grid-cols-3 sm:gap-3">
<Metric label="Contract" value={formatCurrency(site.contractValue)} />
<Metric
label="Spent"
@ -262,7 +267,10 @@ function Metric({
sub?: string;
}) {
return (
<div>
// `min-w-0`: without it, a grid item won't shrink below its content's
// natural width, so a wide currency value (e.g. "R 24 988 663") spills
// into the next column instead of wrapping at 375px.
<div className="min-w-0">
<p className="text-[10px] uppercase tracking-wider text-muted-foreground">
{label}
</p>

View file

@ -328,7 +328,12 @@ export function Topbar({ title, subtitle, right }: { title: string; subtitle?: s
</React.Suspense>
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
{/* flex-wrap: a page's `right` prop can carry more than one
labelled button (e.g. the vehicles page's "Edit descriptions"
+ "Import fuel log"), which together with the scope dropdown
and theme toggle is wider than a 375px screen — without
wrapping, the overflow pushed the whole page sideways. */}
<div className="flex shrink-0 flex-wrap items-center gap-2">
<div className="hidden lg:block">
<React.Suspense fallback={null}>
<SiteScopeSwitcher variant="dropdown" />

View file

@ -34,7 +34,12 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-border bg-card p-6 shadow-xl rounded-xl",
// `max-h-[90dvh] overflow-y-auto`: a dialog taller than the
// viewport (long forms, big tables) used to extend off both the
// top and bottom on phones with no way to reach content or the
// close button — cap the height and let the content scroll
// internally instead.
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg max-h-[90dvh] -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto border border-border bg-card p-6 shadow-xl rounded-xl",
className
)}
{...props}