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