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:
BOHA
2026-06-07 21:44:22 +02:00
parent 54f3c414f5
commit decadd895e
10 changed files with 41 additions and 59 deletions

View File

@@ -64,6 +64,7 @@ import {
LoadingState, LoadingState,
PageEnter, PageEnter,
RichTextView, RichTextView,
headerActionsSx,
} from "../ui"; } from "../ui";
const API_BASE = "/api/admin"; const API_BASE = "/api/admin";
@@ -1114,14 +1115,7 @@ export default function InvoiceDetail() {
/> />
</Box> </Box>
</Box> </Box>
<Box <Box sx={headerActionsSx}>
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "wrap",
}}
>
{hasPermission("invoices.export") && ( {hasPermission("invoices.export") && (
<Button <Button
onClick={() => handleViewPdf(invoice.language || "cs")} onClick={() => handleViewPdf(invoice.language || "cs")}
@@ -1660,14 +1654,7 @@ export default function InvoiceDetail() {
)} )}
</Box> </Box>
</Box> </Box>
<Box <Box sx={headerActionsSx}>
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "wrap",
}}
>
{isEdit && invoice && hasPermission("invoices.export") && ( {isEdit && invoice && hasPermission("invoices.export") && (
<Button <Button
onClick={() => handleViewPdf(invoice.language || "cs")} onClick={() => handleViewPdf(invoice.language || "cs")}

View File

@@ -74,6 +74,7 @@ import {
EmptyState, EmptyState,
LoadingState, LoadingState,
PageEnter, PageEnter,
headerActionsSx,
} from "../ui"; } from "../ui";
const API_BASE = "/api/admin"; const API_BASE = "/api/admin";
@@ -1118,14 +1119,7 @@ export default function OfferDetail() {
{isCompleted && <StatusChip label="Dokončeno" color="success" />} {isCompleted && <StatusChip label="Dokončeno" color="success" />}
</Box> </Box>
</Box> </Box>
<Box <Box sx={headerActionsSx}>
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "wrap",
}}
>
{isEdit && hasPermission("offers.export") && ( {isEdit && hasPermission("offers.export") && (
<Button <Button
onClick={handlePdf} onClick={handlePdf}

View File

@@ -32,6 +32,7 @@ import {
LoadingState, LoadingState,
PageEnter, PageEnter,
RichTextView, RichTextView,
headerActionsSx,
type DataColumn, type DataColumn,
} from "../ui"; } from "../ui";
@@ -424,14 +425,7 @@ export default function OrderDetail() {
/> />
</Box> </Box>
</Box> </Box>
<Box <Box sx={headerActionsSx}>
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "wrap",
}}
>
{order.invoice ? ( {order.invoice ? (
<Button <Button
component={RouterLink} component={RouterLink}

View File

@@ -29,6 +29,7 @@ import {
ConfirmDialog, ConfirmDialog,
LoadingState, LoadingState,
PageEnter, PageEnter,
headerActionsSx,
} from "../ui"; } from "../ui";
const API_BASE = "/api/admin"; const API_BASE = "/api/admin";
@@ -320,14 +321,7 @@ export default function ProjectDetail() {
</Box> </Box>
</Box> </Box>
{canEdit && ( {canEdit && (
<Box <Box sx={headerActionsSx}>
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "wrap",
}}
>
<Button onClick={handleSave} disabled={saving}> <Button onClick={handleSave} disabled={saving}>
{saving ? "Ukládání..." : "Uložit"} {saving ? "Ukládání..." : "Uložit"}
</Button> </Button>

View File

@@ -24,6 +24,7 @@ import {
PageHeader, PageHeader,
PageEnter, PageEnter,
ConfirmDialog, ConfirmDialog,
headerActionsSx,
type DataColumn, type DataColumn,
} from "../ui"; } from "../ui";
@@ -192,7 +193,7 @@ export default function WarehouseInventoryDetail() {
<PageHeader <PageHeader
title={s.session_number || "Inventura"} title={s.session_number || "Inventura"}
actions={ actions={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={headerActionsSx}>
<Button <Button
component={RouterLink} component={RouterLink}
to="/warehouse/inventory" to="/warehouse/inventory"

View File

@@ -24,6 +24,7 @@ import {
PageHeader, PageHeader,
ConfirmDialog, ConfirmDialog,
PageEnter, PageEnter,
headerActionsSx,
type DataColumn, type DataColumn,
} from "../ui"; } from "../ui";
@@ -233,7 +234,7 @@ export default function WarehouseIssueDetail() {
: undefined : undefined
} }
actions={ actions={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={headerActionsSx}>
<Button <Button
component={RouterLink} component={RouterLink}
to="/warehouse/issues" to="/warehouse/issues"

View File

@@ -29,6 +29,7 @@ import {
PageHeader, PageHeader,
ConfirmDialog, ConfirmDialog,
PageEnter, PageEnter,
headerActionsSx,
type DataColumn, type DataColumn,
} from "../ui"; } from "../ui";
@@ -233,7 +234,7 @@ export default function WarehouseItemDetail() {
// Edit / view mode action buttons // Edit / view mode action buttons
const headerActions = canManage ? ( const headerActions = canManage ? (
<Box sx={{ display: "flex", gap: 1 }}> <Box sx={headerActionsSx}>
{editing || isNew ? ( {editing || isNew ? (
<> <>
{!isNew && ( {!isNew && (
@@ -362,7 +363,7 @@ export default function WarehouseItemDetail() {
title={title} title={title}
subtitle={subtitle} subtitle={subtitle}
actions={ actions={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={headerActionsSx}>
<Button <Button
component={RouterLink} component={RouterLink}
to="/warehouse/items" to="/warehouse/items"

View File

@@ -26,6 +26,7 @@ import {
PageHeader, PageHeader,
ConfirmDialog, ConfirmDialog,
PageEnter, PageEnter,
headerActionsSx,
type DataColumn, type DataColumn,
} from "../ui"; } from "../ui";
@@ -301,7 +302,7 @@ export default function WarehouseReceiptDetail() {
title={r.receipt_number || "Nový doklad"} title={r.receipt_number || "Nový doklad"}
subtitle={r.supplier?.name} subtitle={r.supplier?.name}
actions={ actions={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={headerActionsSx}>
<Button <Button
component={RouterLink} component={RouterLink}
to="/warehouse/receipts" to="/warehouse/receipts"

View File

@@ -1,6 +1,7 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import type { SxProps, Theme } from "@mui/material/styles";
export interface PageHeaderProps { export interface PageHeaderProps {
title: string; title: string;
@@ -8,6 +9,26 @@ export interface PageHeaderProps {
actions?: ReactNode; 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, * Standard page header: title (+ optional subtitle) on the left,
* action controls on the right. No framer-motion — pages add their own entrance. * action controls on the right. No framer-motion — pages add their own entrance.
@@ -36,19 +57,7 @@ export default function PageHeader({
</Typography> </Typography>
)} )}
</Box> </Box>
{actions && ( {actions && <Box sx={headerActionsSx}>{actions}</Box>}
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 1,
flexWrap: "wrap",
}}
>
{actions}
</Box>
)}
</Box> </Box>
); );
} }

View File

@@ -18,7 +18,7 @@ export { default as Pagination } from "./Pagination";
export { default as DateField } from "./DateField"; export { default as DateField } from "./DateField";
export { default as MonthField } from "./MonthField"; export { default as MonthField } from "./MonthField";
export { default as TimeField } from "./TimeField"; 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 EmptyState } from "./EmptyState";
export { default as LoadingState } from "./LoadingState"; export { default as LoadingState } from "./LoadingState";
export { default as FilterBar } from "./FilterBar"; export { default as FilterBar } from "./FilterBar";