Files
supabase/apps/studio/components/ui/FeatureBanner.tsx
2026-04-01 10:22:37 +02:00

89 lines
2.4 KiB
TypeScript

import { useParams } from 'common/hooks'
import { HTMLMotionProps, motion } from 'framer-motion'
import { X } from 'lucide-react'
import { ReactNode } from 'react'
import { Button, cn } from 'ui'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
// Base props common to all feature banners
interface BaseFeatureBannerProps extends HTMLMotionProps<'div'> {
children: ReactNode
className?: string
dismissClassName?: string
defaultDismissed?: boolean
illustration?: ReactNode
bgAlt?: boolean
}
// Type for non-dismissable banners (no storageKey needed)
interface NonDismissableFeatureBannerProps extends BaseFeatureBannerProps {
dismissable?: false
storageKey?: never
}
// Type for dismissable banners (requires storageKey)
interface DismissableFeatureBannerProps extends BaseFeatureBannerProps {
dismissable: true
storageKey: string | ((ref: string) => string)
}
// Union type that enforces the constraint
export type FeatureBannerProps = NonDismissableFeatureBannerProps | DismissableFeatureBannerProps
export const FeatureBanner = ({
storageKey,
children,
className,
dismissClassName,
defaultDismissed = false,
illustration,
dismissable = false,
bgAlt = false,
...props
}: FeatureBannerProps) => {
const { ref } = useParams()
const key = storageKey && typeof storageKey === 'function' ? storageKey(ref ?? '') : storageKey
const [isDismissed, setIsDismissed] = useLocalStorageQuery(
key || 'feature-banner-dismissed',
defaultDismissed
)
if (dismissable && storageKey && isDismissed) return null
return (
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: 'spring',
stiffness: 500,
damping: 30,
mass: 1,
}}
{...props}
className={cn(
'pb-36 pt-10 relative w-full border xl:py-10 px-10 rounded-md overflow-hidden',
bgAlt && 'bg-background-alternative',
className
)}
>
{children}
{illustration}
{dismissable && storageKey && (
<div className={cn('absolute top-3 right-3', dismissClassName)}>
<Button
type="text"
size="tiny"
icon={<X size={16} strokeWidth={1.5} />}
onClick={() => setIsDismissed(true)}
className="opacity-75 px-1"
aria-label="Dismiss notification"
/>
</div>
)}
</motion.div>
)
}