PageHeader.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import { useParams } from 'common'
  2. import { ChevronLeft } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { Fragment, ReactNode } from 'react'
  5. import { cn } from 'ui'
  6. import {
  7. Breadcrumb,
  8. BreadcrumbItem,
  9. BreadcrumbLink,
  10. BreadcrumbList,
  11. BreadcrumbPage as BreadcrumbPageItem,
  12. BreadcrumbSeparator,
  13. } from 'ui/src/components/shadcn/ui/breadcrumb'
  14. import { ScaffoldDescription, ScaffoldTitle } from '../Scaffold'
  15. interface PageHeaderProps {
  16. title?: string | ReactNode
  17. subtitle?: string | ReactNode
  18. icon?: ReactNode
  19. breadcrumbs?: Array<{
  20. label?: string
  21. href?: string
  22. element?: ReactNode
  23. }>
  24. primaryActions?: ReactNode
  25. secondaryActions?: ReactNode
  26. className?: string
  27. isCompact?: boolean
  28. }
  29. export const PageHeader = ({
  30. title,
  31. subtitle,
  32. icon,
  33. breadcrumbs = [],
  34. primaryActions,
  35. secondaryActions,
  36. className,
  37. isCompact = false,
  38. }: PageHeaderProps) => {
  39. const { ref } = useParams()
  40. const displayBreadcrumbs = isCompact && title ? [...breadcrumbs, { label: title }] : breadcrumbs
  41. return (
  42. <div className={cn('space-y-4', className)}>
  43. {(displayBreadcrumbs.length > 0 ||
  44. (isCompact && (title || primaryActions || secondaryActions))) && (
  45. <div className={cn('flex items-center gap-4', isCompact ? 'justify-between' : 'mb-4')}>
  46. <div className="flex items-center gap-4 flex-1 min-w-0">
  47. {breadcrumbs.length > 0 ? (
  48. <Breadcrumb
  49. className={cn('text-foreground-muted', isCompact && 'text-base', 'min-w-0 flex-1')}
  50. >
  51. <BreadcrumbList className={cn(isCompact ? 'text-base' : 'text-xs', 'min-w-0')}>
  52. {breadcrumbs.map((item, index) => (
  53. <Fragment key={item.label || `breadcrumb-${index}`}>
  54. <BreadcrumbItem>
  55. {item.element ? (
  56. item.element
  57. ) : item.href ? (
  58. <BreadcrumbLink asChild className="flex items-center gap-2">
  59. <Link href={!!ref ? item.href.replace('[ref]', ref) : item.href}>
  60. {breadcrumbs.length === 1 && !isCompact && (
  61. <ChevronLeft size={16} strokeWidth={1.5} />
  62. )}
  63. {item.label}
  64. </Link>
  65. </BreadcrumbLink>
  66. ) : (
  67. <BreadcrumbPageItem className="flex items-center gap-2">
  68. {breadcrumbs.length === 1 && (
  69. <ChevronLeft size={16} strokeWidth={1.5} />
  70. )}
  71. {item.label}
  72. </BreadcrumbPageItem>
  73. )}
  74. </BreadcrumbItem>
  75. {index < breadcrumbs.length - 1 && <BreadcrumbSeparator />}
  76. </Fragment>
  77. ))}
  78. {isCompact && title && (
  79. <>
  80. <BreadcrumbSeparator />
  81. <BreadcrumbItem className="min-w-0 flex-1">
  82. <BreadcrumbPageItem className="min-w-0">{title}</BreadcrumbPageItem>
  83. </BreadcrumbItem>
  84. </>
  85. )}
  86. </BreadcrumbList>
  87. </Breadcrumb>
  88. ) : isCompact ? (
  89. <div className="min-w-0 flex-1">{title}</div>
  90. ) : null}
  91. </div>
  92. {isCompact && (
  93. <div className="flex items-center gap-2 shrink-0">
  94. {secondaryActions && (
  95. <div className="flex items-center gap-2">{secondaryActions}</div>
  96. )}
  97. {primaryActions && <div className="flex items-center gap-2">{primaryActions}</div>}
  98. </div>
  99. )}
  100. </div>
  101. )}
  102. {!isCompact && (
  103. <div className="flex items-center justify-between gap-4">
  104. <div className="space-y-4">
  105. <div className="flex items-center gap-4">
  106. {icon && <div className="text-foreground-light">{icon}</div>}
  107. <div className="space-y-1">
  108. {title &&
  109. (typeof title === 'string' ? <ScaffoldTitle>{title}</ScaffoldTitle> : title)}
  110. {subtitle &&
  111. (typeof subtitle === 'string' ? (
  112. <ScaffoldDescription className="text-sm text-foreground-light">
  113. {subtitle}
  114. </ScaffoldDescription>
  115. ) : (
  116. subtitle
  117. ))}
  118. </div>
  119. </div>
  120. </div>
  121. <div className="flex items-center gap-2">
  122. {secondaryActions && <div className="flex items-center gap-2">{secondaryActions}</div>}
  123. {primaryActions && <div className="flex items-center gap-2">{primaryActions}</div>}
  124. </div>
  125. </div>
  126. )}
  127. </div>
  128. )
  129. }