fix(ui): stack detail-page header action buttons one-per-row on mobile
Detail-page headers put their action buttons in a flex row with flexWrap:"wrap", so on a phone the 2-4 buttons (Potvrzení / Zahájit realizaci / Smazat, etc.) wrapped into a ragged grid. New shared `headerActionsSx` (ui/PageHeader): on xs it's a full-width column (each button its own full-width row); from sm up it's the usual right-aligned wrapping row — desktop unchanged. A StatusChip kept in the same group stays its natural size (alignSelf) instead of stretching. Applied to PageHeader's own actions slot and every detail-page header action group: Order/Offer/Invoice (both views)/Project + the Warehouse Receipt/Issue/Item/Inventory detail headers. Verified in Chrome at 430px on OrderDetail: the group is flexDirection column with all three buttons at the same x, full-width, stacked. tsc -b --noEmit, npm run build, vitest 152/152 clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ import {
|
||||
LoadingState,
|
||||
PageEnter,
|
||||
RichTextView,
|
||||
headerActionsSx,
|
||||
} from "../ui";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
@@ -1114,14 +1115,7 @@ export default function InvoiceDetail() {
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box sx={headerActionsSx}>
|
||||
{hasPermission("invoices.export") && (
|
||||
<Button
|
||||
onClick={() => handleViewPdf(invoice.language || "cs")}
|
||||
@@ -1660,14 +1654,7 @@ export default function InvoiceDetail() {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box sx={headerActionsSx}>
|
||||
{isEdit && invoice && hasPermission("invoices.export") && (
|
||||
<Button
|
||||
onClick={() => handleViewPdf(invoice.language || "cs")}
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
EmptyState,
|
||||
LoadingState,
|
||||
PageEnter,
|
||||
headerActionsSx,
|
||||
} from "../ui";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
@@ -1118,14 +1119,7 @@ export default function OfferDetail() {
|
||||
{isCompleted && <StatusChip label="Dokončeno" color="success" />}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box sx={headerActionsSx}>
|
||||
{isEdit && hasPermission("offers.export") && (
|
||||
<Button
|
||||
onClick={handlePdf}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
LoadingState,
|
||||
PageEnter,
|
||||
RichTextView,
|
||||
headerActionsSx,
|
||||
type DataColumn,
|
||||
} from "../ui";
|
||||
|
||||
@@ -424,14 +425,7 @@ export default function OrderDetail() {
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box sx={headerActionsSx}>
|
||||
{order.invoice ? (
|
||||
<Button
|
||||
component={RouterLink}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ConfirmDialog,
|
||||
LoadingState,
|
||||
PageEnter,
|
||||
headerActionsSx,
|
||||
} from "../ui";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
@@ -320,14 +321,7 @@ export default function ProjectDetail() {
|
||||
</Box>
|
||||
</Box>
|
||||
{canEdit && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box sx={headerActionsSx}>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
{saving ? "Ukládání..." : "Uložit"}
|
||||
</Button>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
PageHeader,
|
||||
PageEnter,
|
||||
ConfirmDialog,
|
||||
headerActionsSx,
|
||||
type DataColumn,
|
||||
} from "../ui";
|
||||
|
||||
@@ -192,7 +193,7 @@ export default function WarehouseInventoryDetail() {
|
||||
<PageHeader
|
||||
title={s.session_number || "Inventura"}
|
||||
actions={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Box sx={headerActionsSx}>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/warehouse/inventory"
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
PageHeader,
|
||||
ConfirmDialog,
|
||||
PageEnter,
|
||||
headerActionsSx,
|
||||
type DataColumn,
|
||||
} from "../ui";
|
||||
|
||||
@@ -233,7 +234,7 @@ export default function WarehouseIssueDetail() {
|
||||
: undefined
|
||||
}
|
||||
actions={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Box sx={headerActionsSx}>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/warehouse/issues"
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
PageHeader,
|
||||
ConfirmDialog,
|
||||
PageEnter,
|
||||
headerActionsSx,
|
||||
type DataColumn,
|
||||
} from "../ui";
|
||||
|
||||
@@ -233,7 +234,7 @@ export default function WarehouseItemDetail() {
|
||||
|
||||
// Edit / view mode action buttons
|
||||
const headerActions = canManage ? (
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Box sx={headerActionsSx}>
|
||||
{editing || isNew ? (
|
||||
<>
|
||||
{!isNew && (
|
||||
@@ -362,7 +363,7 @@ export default function WarehouseItemDetail() {
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
actions={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Box sx={headerActionsSx}>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/warehouse/items"
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
PageHeader,
|
||||
ConfirmDialog,
|
||||
PageEnter,
|
||||
headerActionsSx,
|
||||
type DataColumn,
|
||||
} from "../ui";
|
||||
|
||||
@@ -301,7 +302,7 @@ export default function WarehouseReceiptDetail() {
|
||||
title={r.receipt_number || "Nový doklad"}
|
||||
subtitle={r.supplier?.name}
|
||||
actions={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Box sx={headerActionsSx}>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/warehouse/receipts"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
|
||||
export interface PageHeaderProps {
|
||||
title: string;
|
||||
@@ -8,6 +9,26 @@ export interface PageHeaderProps {
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared sx for a header action-button group. On mobile each button gets its
|
||||
* own full-width row (column + stretch); from `sm` up it's the usual right-
|
||||
* aligned wrapping row. Use on the Box that wraps the header's action Buttons
|
||||
* (page headers + detail-page headers) so the mobile layout is identical
|
||||
* everywhere — no more buttons wrapping into a ragged grid on phones.
|
||||
*/
|
||||
export const headerActionsSx: SxProps<Theme> = {
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "stretch", sm: "center" },
|
||||
justifyContent: { sm: "flex-end" },
|
||||
flexWrap: { sm: "wrap" },
|
||||
gap: 1,
|
||||
width: { xs: "100%", sm: "auto" },
|
||||
// Some detail-page headers keep a StatusChip in this same group — keep it at
|
||||
// its natural size on mobile instead of stretching it into a full-width bar.
|
||||
"& > .MuiChip-root": { alignSelf: { xs: "flex-start", sm: "auto" } },
|
||||
};
|
||||
|
||||
/**
|
||||
* Standard page header: title (+ optional subtitle) on the left,
|
||||
* action controls on the right. No framer-motion — pages add their own entrance.
|
||||
@@ -36,19 +57,7 @@ export default function PageHeader({
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{actions && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
gap: 1,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{actions}
|
||||
</Box>
|
||||
)}
|
||||
{actions && <Box sx={headerActionsSx}>{actions}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export { default as Pagination } from "./Pagination";
|
||||
export { default as DateField } from "./DateField";
|
||||
export { default as MonthField } from "./MonthField";
|
||||
export { default as TimeField } from "./TimeField";
|
||||
export { default as PageHeader } from "./PageHeader";
|
||||
export { default as PageHeader, headerActionsSx } from "./PageHeader";
|
||||
export { default as EmptyState } from "./EmptyState";
|
||||
export { default as LoadingState } from "./LoadingState";
|
||||
export { default as FilterBar } from "./FilterBar";
|
||||
|
||||
Reference in New Issue
Block a user