347 lines
10 KiB
TypeScript
347 lines
10 KiB
TypeScript
import React, { ReactNode, useCallback, useMemo } from 'react';
|
||
import {
|
||
Title,
|
||
TitleProps,
|
||
Breadcrumbs,
|
||
Anchor,
|
||
Box,
|
||
Text,
|
||
ThemeIcon,
|
||
Flex,
|
||
Divider,
|
||
ActionIcon,
|
||
Tooltip,
|
||
} from '@mantine/core';
|
||
import { useLocalStorage } from '@mantine/hooks';
|
||
import { Link } from 'react-router-dom';
|
||
import { ChevronRight, LucideIcon, Maximize2, Minimize2 } from 'lucide-react'; // <-- Update Import Icon
|
||
import { PageActions, PageActionsProps } from '../../../../components';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Types
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export interface BreadcrumbItem {
|
||
label: string;
|
||
href?: string;
|
||
type: 'text' | 'link';
|
||
}
|
||
|
||
export interface ModulePageHeaderProps {
|
||
title?: ReactNode;
|
||
titleProps?: Omit<TitleProps, 'children'>;
|
||
miniTitleProps?: Omit<TitleProps, 'children'>;
|
||
description?: React.ReactNode;
|
||
icon?: LucideIcon;
|
||
|
||
badges?: React.ReactNode;
|
||
showBadges?: boolean;
|
||
breadcrumbs?: BreadcrumbItem[];
|
||
|
||
showPageHeader?: boolean;
|
||
disableMinimize?: boolean;
|
||
moduleKey: string;
|
||
|
||
actions?: PageActionsProps['actions'];
|
||
customButtonProps?: PageActionsProps['customButtonProps'];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Shared transition style applied to all animated wrappers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const TRANSITION_STYLE: React.CSSProperties = {
|
||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Sub-components
|
||
// ---------------------------------------------------------------------------
|
||
|
||
interface BreadcrumbBarProps {
|
||
breadcrumbs: BreadcrumbItem[];
|
||
}
|
||
|
||
function BreadcrumbBar({ breadcrumbs }: BreadcrumbBarProps) {
|
||
return (
|
||
<Breadcrumbs
|
||
style={{ flexWrap: 'wrap' }}
|
||
// visibleFrom="xs"
|
||
separator={
|
||
<ChevronRight
|
||
size={12}
|
||
strokeWidth={3}
|
||
style={{
|
||
color: 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))',
|
||
}}
|
||
/>
|
||
}
|
||
>
|
||
{breadcrumbs.map((item, index) => {
|
||
const isText = item.type === 'text';
|
||
const isLast = index === breadcrumbs.length - 1;
|
||
const sharedProps = {
|
||
c: !isLast ? ('dimmed' as const) : undefined,
|
||
size: 'xs' as const,
|
||
fw: 500,
|
||
style: {
|
||
color: isLast ? 'var(--mantine-color-text)' : undefined,
|
||
transition: 'color 0.2s ease',
|
||
letterSpacing: '0.2px',
|
||
},
|
||
};
|
||
|
||
return !isText ? (
|
||
<Anchor component={Link} key={index} {...sharedProps} to={item.href || '#'}>
|
||
{item.label}
|
||
</Anchor>
|
||
) : (
|
||
<Text key={index} {...sharedProps}>
|
||
{item.label}
|
||
</Text>
|
||
);
|
||
})}
|
||
</Breadcrumbs>
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// HeaderToggle
|
||
// ---------------------------------------------------------------------------
|
||
|
||
interface HeaderToggleProps {
|
||
isMinimized: boolean;
|
||
onToggle: () => void;
|
||
}
|
||
|
||
function HeaderToggle({ isMinimized, onToggle }: HeaderToggleProps) {
|
||
// const ToggleIcon = isMinimized ? ChevronDown : ChevronUp;
|
||
const ToggleIcon = isMinimized ? Maximize2 : Minimize2;
|
||
|
||
const label = isMinimized ? 'Expand header' : 'Collapse header';
|
||
|
||
return (
|
||
<Tooltip label={label} position="bottom-end" withArrow openDelay={400}>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
size="sm"
|
||
radius="md"
|
||
onClick={onToggle}
|
||
aria-label={label}
|
||
style={{
|
||
opacity: 0.6,
|
||
...TRANSITION_STYLE,
|
||
}}
|
||
>
|
||
<ToggleIcon size={13} strokeWidth={2} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Main component
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
||
const {
|
||
title,
|
||
titleProps,
|
||
miniTitleProps,
|
||
description,
|
||
icon: Icon,
|
||
badges,
|
||
showBadges = true,
|
||
|
||
breadcrumbs,
|
||
actions,
|
||
customButtonProps,
|
||
showPageHeader = true,
|
||
disableMinimize = false,
|
||
moduleKey,
|
||
} = _props;
|
||
|
||
const [isMinimized, setIsMinimized] = useLocalStorage<boolean>({
|
||
key: `page-header-minimized__${btoa(moduleKey)}`,
|
||
defaultValue: false,
|
||
getInitialValueInEffect: false,
|
||
});
|
||
|
||
if (!showPageHeader) {
|
||
return null;
|
||
}
|
||
|
||
const showToggle = !disableMinimize;
|
||
const handleToggle = useCallback(() => setIsMinimized((v) => !v), [setIsMinimized]);
|
||
const compactButtonProps = useMemo(
|
||
() => (action: any) => {
|
||
if (customButtonProps) return customButtonProps(action);
|
||
return { size: 'xs' as const };
|
||
},
|
||
[customButtonProps],
|
||
);
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Compact / Toolbar mode
|
||
// -------------------------------------------------------------------------
|
||
if (isMinimized) {
|
||
return (
|
||
<Box mb={0} mt="xs" pb={6} style={TRANSITION_STYLE} className="module-page-header">
|
||
<Flex
|
||
direction="row"
|
||
justify="space-between"
|
||
align="center"
|
||
gap="sm"
|
||
wrap="nowrap"
|
||
style={{ minHeight: 36, ...TRANSITION_STYLE }}
|
||
>
|
||
{/* Left cluster: title + badges */}
|
||
<Flex gap="xs" align="center" style={{ flex: 1, minWidth: 0 }}>
|
||
{title && (
|
||
<Title
|
||
fw={600}
|
||
fz={{ base: 'lg', sm: 'xl' }}
|
||
lineClamp={1}
|
||
{...(miniTitleProps ?? {})}
|
||
style={{
|
||
color: 'var(--mantine-color-text)',
|
||
letterSpacing: '-0.2px',
|
||
lineHeight: 1.3,
|
||
...TRANSITION_STYLE,
|
||
...miniTitleProps?.style,
|
||
}}
|
||
>
|
||
{title}
|
||
</Title>
|
||
)}
|
||
{showBadges && badges && <Box style={{ flexShrink: 0 }}>{badges}</Box>}
|
||
</Flex>
|
||
|
||
{/* Right: actions + inline toggle (separated by divider) */}
|
||
<Flex align="center" gap="sm" style={{ flexShrink: 0 }}>
|
||
{actions && <PageActions actions={actions} customButtonProps={compactButtonProps} />}
|
||
{showToggle && (
|
||
<>
|
||
{actions && actions.length > 0 && (
|
||
<Divider orientation="vertical" style={{ height: 20, opacity: 0.4 }} />
|
||
)}
|
||
<HeaderToggle isMinimized={isMinimized} onToggle={handleToggle} />
|
||
</>
|
||
)}
|
||
</Flex>
|
||
</Flex>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Expanded / Full mode
|
||
// -------------------------------------------------------------------------
|
||
|
||
const hasBreadcrumbs = breadcrumbs && breadcrumbs.length > 0;
|
||
|
||
return (
|
||
<Box mb={0} mt="xs" pb={18} style={TRANSITION_STYLE} className="module-page-header">
|
||
{/* 1. Top Navigation Row: Breadcrumbs & Toggle */}
|
||
{((breadcrumbs && breadcrumbs.length) || showToggle) && (
|
||
<Flex
|
||
align="center"
|
||
justify="space-between"
|
||
mb={{ base: showToggle ? 'xs' : 0, sm: hasBreadcrumbs || showToggle ? 'md' : 0 }}
|
||
>
|
||
<Box>{hasBreadcrumbs && <BreadcrumbBar breadcrumbs={breadcrumbs} />}</Box>
|
||
{showToggle && (
|
||
<Box ml="auto">
|
||
<HeaderToggle isMinimized={isMinimized} onToggle={handleToggle} />
|
||
</Box>
|
||
)}
|
||
</Flex>
|
||
)}
|
||
|
||
{/* 2. Main Header Row – 3-column layout */}
|
||
<Flex
|
||
direction="row"
|
||
justify="space-between"
|
||
align={{ base: 'stretch', sm: 'flex-start' }}
|
||
gap={{ base: 'md', sm: 'md' }}
|
||
wrap="nowrap"
|
||
style={TRANSITION_STYLE}
|
||
>
|
||
<Flex gap="md" align={{ base: 'flex-start', sm: 'flex-start' }} style={{ flex: 1, minWidth: 0 }}>
|
||
{Icon && (
|
||
<ThemeIcon
|
||
w={{ base: 40, sm: 48 }}
|
||
h={{ base: 40, sm: 48 }}
|
||
radius="md"
|
||
variant="light"
|
||
color="brand"
|
||
visibleFrom="sm"
|
||
style={{
|
||
flexShrink: 0,
|
||
border: '1px solid light-dark(var(--mantine-color-brand-1), var(--mantine-color-brand-6))',
|
||
boxShadow: 'light-dark(0 4px 12px rgba(0,0,0,0.03), 0 4px 12px rgba(0,0,0,0.2))',
|
||
...TRANSITION_STYLE,
|
||
}}
|
||
>
|
||
<Icon size={24} strokeWidth={1.5} />
|
||
</ThemeIcon>
|
||
)}
|
||
|
||
{(title || badges || description) && (
|
||
<Box style={{ flex: 1, minWidth: 0, ...TRANSITION_STYLE }}>
|
||
<Flex gap="xs" align="center" wrap="wrap">
|
||
{title && (
|
||
<Title
|
||
order={2}
|
||
fw={600}
|
||
lineClamp={1}
|
||
fz={{ base: 20, sm: 24 }}
|
||
lh={{ base: 1.3, sm: 1.2 }}
|
||
{...(titleProps ?? {})}
|
||
style={{
|
||
color: 'var(--mantine-color-text)',
|
||
letterSpacing: '-0.3px',
|
||
...TRANSITION_STYLE,
|
||
...titleProps?.style,
|
||
}}
|
||
>
|
||
{title}
|
||
</Title>
|
||
)}
|
||
{showBadges && badges && <Box>{badges}</Box>}
|
||
</Flex>
|
||
|
||
{description && (
|
||
<Text
|
||
fz={{ base: 'xs', sm: 'sm' }}
|
||
c="dimmed"
|
||
fw={400}
|
||
lineClamp={1}
|
||
mt={4}
|
||
style={{
|
||
wordBreak: 'break-word',
|
||
...TRANSITION_STYLE,
|
||
}}
|
||
>
|
||
{description}
|
||
</Text>
|
||
)}
|
||
</Box>
|
||
)}
|
||
</Flex>
|
||
|
||
<Flex
|
||
align="center"
|
||
gap="xs"
|
||
mt={{ base: 'xs', sm: 0 }}
|
||
ml={{ base: 'lg', sm: 0 }}
|
||
style={{ flexShrink: 0, alignSelf: 'center' }}
|
||
>
|
||
{actions && <PageActions actions={actions} customButtonProps={customButtonProps} />}
|
||
</Flex>
|
||
</Flex>
|
||
</Box>
|
||
);
|
||
}
|