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,
|
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")}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user