From f0b5fb8a218d7fc8c48005d490e9b4950cf6cdc2 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sun, 7 Jun 2026 16:20:08 +0200 Subject: [PATCH] feat(ui): DataTable renders a card layout on phones instead of a scrolling table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On screens below the sm breakpoint, a wide multi-column table forced horizontal scrolling and truncated every cell (e.g. 'NA-202…'). DataTable now renders each row as a stacked label:value card on phones: every column becomes a 'HEADER: value' line with full, wrapping content; the actions column (key 'actions') drops to a right-aligned button row at the bottom; rowDanger/rowInactive/rowSx/onRowClick are all honored (action buttons stopPropagation so they don't trigger the row click). Tablets/desktop (>= sm) keep the normal table. Applies to every list table app-wide. Verified on Offers (actions) and Warehouse items (onRowClick): no truncation, no page horizontal scroll. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/admin/ui/DataTable.tsx | 103 +++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/admin/ui/DataTable.tsx b/src/admin/ui/DataTable.tsx index 65d44bc..ec39fd3 100644 --- a/src/admin/ui/DataTable.tsx +++ b/src/admin/ui/DataTable.tsx @@ -1,5 +1,9 @@ import type { ReactNode } from "react"; import type { SxProps, Theme } from "@mui/material/styles"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; @@ -60,10 +64,109 @@ export default function DataTable({ sortDir, onSort, }: DataTableProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); if (import.meta.env.DEV && columns.some((c) => c.sortKey) && !onSort) { console.warn("DataTable: columns with sortKey require an onSort prop."); } if (rows.length === 0 && empty) return <>{empty}; + + // On phones a wide multi-column table would force horizontal scrolling and + // truncate every cell. Instead render each row as a stacked "label: value" + // card so all content is readable; the actions column (key "actions") drops + // to a button row at the bottom. + if (isMobile) { + const actionCol = columns.find((c) => c.key === "actions"); + const dataCols = columns.filter((c) => c.key !== "actions"); + return ( + + {rows.map((row) => { + const extra = rowSx?.(row); + return ( + onRowClick(row) : undefined} + sx={[ + { + border: 1, + borderColor: "divider", + borderRadius: 2, + p: 1.5, + display: "flex", + flexDirection: "column", + gap: 0.75, + bgcolor: "background.paper", + }, + onRowClick ? { cursor: "pointer" } : {}, + rowDanger?.(row) + ? { borderColor: "error.main", bgcolor: "error.light" } + : {}, + rowInactive?.(row) ? { opacity: 0.55 } : {}, + ...(Array.isArray(extra) ? extra : extra ? [extra] : []), + ]} + > + {dataCols.map((c) => ( + + + {c.header} + + + {c.render(row)} + + + ))} + {actionCol && ( + e.stopPropagation()} + sx={{ + display: "flex", + flexWrap: "wrap", + justifyContent: "flex-end", + gap: 0.5, + pt: 0.75, + mt: 0.25, + borderTop: 1, + borderColor: "divider", + }} + > + {actionCol.render(row)} + + )} + + ); + })} + + ); + } // When any column declares a width, lock the table to fixed layout so column // widths come from the colgroup (stable) instead of from cell content (which // jumps as rows are filtered). Tables that set no widths keep auto layout.