Files
edr-platform/apps/edr-freight-web/backoffice/src/components/page/PageHeader.tsx
Nathnael de407c0014 feat(reports): clamp report descriptions with a show more toggle
Report descriptions run to a paragraph. ReportDescription wraps Mantine's
Spoiler to clamp them to two lines, and the toggle only renders when the
text actually overflows.

PageHeader kept its subtitle on a hard truncate class, which would have
pinned the spoiler to one line, so a ReactNode subtitle now renders as-is
and owns its own layout. A string subtitle still truncates as before.
2026-08-24 07:54:36 +00:00

85 lines
2.6 KiB
TypeScript

import { ActionIcon, Group, Stack, Text, Title } from "@mantine/core";
import { ArrowLeft } from "lucide-react";
import type { ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import Breadcrumbs, { type BreadcrumbItem } from "@/components/ui/Breadcrumbs";
export interface PageHeaderProps {
title: string;
subtitle?: ReactNode;
/** Breadcrumb trail — pass only on nested pages (details, sub-resources). */
breadcrumbs?: BreadcrumbItem[];
/** Route to return to; renders a back arrow before the title. */
backTo?: string;
/** Inline content beside the title (e.g. status badges). */
meta?: ReactNode;
/** Right-aligned actions — the primary CTA lives here. */
action?: ReactNode;
}
/**
* Unified page header: optional breadcrumbs, a title (with optional back arrow
* and inline meta), a subtitle, and a right-aligned action slot. Keeps title /
* action placement and spacing identical across every dashboard page.
*/
export function PageHeader({
title,
subtitle,
breadcrumbs,
backTo,
meta,
action,
}: PageHeaderProps) {
const navigate = useNavigate();
return (
<Stack gap="sm">
{breadcrumbs?.length ? <Breadcrumbs items={breadcrumbs} /> : null}
<Group justify="space-between" align="flex-start" gap="md">
<Group gap="sm" align="center" wrap="nowrap" style={{ minWidth: 0 }}>
{backTo ? (
<ActionIcon
variant="subtle"
color="gray"
onClick={() => navigate(backTo)}
aria-label="Go back"
>
<ArrowLeft size={18} />
</ActionIcon>
) : null}
<div style={{ minWidth: 0, maxWidth: 640 }}>
<Group gap="sm" align="center" wrap="nowrap">
<Title order={2} className="truncate" style={{ minWidth: 0 }}>
{title}
</Title>
{meta}
</Group>
{subtitle ? (
typeof subtitle === "string" ? (
<Text c="dimmed" size="sm" mt={4} className="truncate">
{subtitle}
</Text>
) : (
// A component subtitle handles its own layout — truncating it
// to one line would defeat e.g. an expandable description.
<div style={{ marginTop: 4 }}>{subtitle}</div>
)
) : null}
</div>
</Group>
{action ? (
<Group gap="sm" wrap="nowrap">
{action}
</Group>
) : null}
</Group>
</Stack>
);
}
export default PageHeader;