Files
trackgo-fe/packages/ui/src/foundations/enterprise-module/components/module-page-header/index.tsx
T

347 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}