HeaderBanner.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { AnimatePresence, motion } from 'framer-motion'
  2. import { XIcon } from 'lucide-react'
  3. import type { ReactNode } from 'react'
  4. import { Button, cn, CriticalIcon, WarningIcon } from 'ui'
  5. import { useOrganizationRestrictions } from '@/hooks/misc/useOrganizationRestrictions'
  6. const bannerMotionProps = {
  7. initial: { height: 0, opacity: 0 },
  8. animate: { height: 'auto', opacity: 1 },
  9. exit: { height: 0, opacity: 0 },
  10. transition: { duration: 0.2, delay: 0.5 },
  11. } as const
  12. const linkStyles =
  13. '[&_a]:underline [&_a]:underline-offset-2 [&_a]:decoration-foreground-muted/80 [&_a]:hover:decoration-foreground [&_a]:hover:text-foreground [&_a]:transition-all'
  14. export const OrganizationResourceBanner = () => {
  15. const { warnings } = useOrganizationRestrictions()
  16. return (
  17. <AnimatePresence initial={false}>
  18. {warnings.map((warning) => (
  19. <HeaderBanner key={`${warning.variant}-${warning.title}`} {...warning} />
  20. ))}
  21. </AnimatePresence>
  22. )
  23. }
  24. interface HeaderBannerProps {
  25. variant: 'danger' | 'warning' | 'note'
  26. title: string
  27. description: string | ReactNode
  28. onDismiss?: () => void
  29. }
  30. const variantStyles = {
  31. danger: {
  32. banner: 'bg-destructive-200 border-destructive-400',
  33. icon: 'text-destructive-200 bg-destructive-600',
  34. },
  35. warning: {
  36. banner: 'bg-warning-200 border-warning-400',
  37. icon: 'text-warning-200 bg-warning-600',
  38. },
  39. note: {
  40. banner: 'bg-surface-200/25 border-default',
  41. icon: 'text-background bg-foreground',
  42. },
  43. } as const
  44. export const HeaderBanner = ({ variant, title, description, onDismiss }: HeaderBannerProps) => {
  45. const { banner: bannerStyles, icon: iconStyles } = variantStyles[variant]
  46. const Icon = variant === 'danger' ? CriticalIcon : WarningIcon
  47. return (
  48. <motion.div
  49. {...bannerMotionProps}
  50. className={cn('relative border-b overflow-hidden', bannerStyles)}
  51. layout="position"
  52. >
  53. {/* Wrapped content for smooth reveal animation */}
  54. <div className="px-4 py-4 md:py-3 flex items-center md:justify-center">
  55. {/* Striped background */}
  56. <div
  57. className="absolute inset-0 opacity-[1.6%] dark:opacity-[0.8%]"
  58. style={{
  59. background: `repeating-linear-gradient(
  60. 45deg,
  61. currentColor,
  62. currentColor 10px,
  63. transparent 10px,
  64. transparent 20px
  65. )`,
  66. maskImage: 'linear-gradient(to bottom, transparent 0%, black 90%)',
  67. WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 90%)',
  68. }}
  69. />
  70. {/* Icon and text content */}
  71. <div
  72. className={cn(
  73. 'relative items-start md:items-center flex flex-row gap-3 min-w-0',
  74. // Avoid collision with the dismiss button
  75. onDismiss && 'pr-7 md:px-7'
  76. )}
  77. >
  78. <Icon className={cn('shrink-0 w-5 h-5 md:w-4 md:h-4', iconStyles)} />
  79. {/* Title and description */}
  80. <div
  81. className={cn(
  82. 'flex flex-col md:flex-row gap-0.5 md:gap-1.5 text-balance md:flex-nowrap min-w-0 flex-1'
  83. )}
  84. >
  85. {/* Title */}
  86. <p className="text-sm text-foreground font-medium md:truncate">{title}</p>
  87. {/* Description */}
  88. <div className="flex flex-row items-center gap-1.5 min-w-0 md:flex-nowrap text-sm">
  89. <span className="hidden md:inline text-foreground-muted">·</span>
  90. {typeof description === 'string' ? (
  91. <p className="text-foreground-light md:truncate">{description}</p>
  92. ) : (
  93. <div className={cn('text-foreground-light md:truncate', linkStyles)}>
  94. {description}
  95. </div>
  96. )}
  97. </div>
  98. </div>
  99. </div>
  100. {onDismiss && (
  101. <Button
  102. type="text"
  103. size="tiny"
  104. className="opacity-75 z-1 shrink-0 p-0.5 h-auto absolute right-5 md:right-4 top-1/2 -translate-y-1/2"
  105. onClick={onDismiss}
  106. aria-label="Dismiss banner"
  107. >
  108. <XIcon size={16} className="text-foreground" />
  109. </Button>
  110. )}
  111. </div>
  112. </motion.div>
  113. )
  114. }