import { useThrottle } from '@/hooks/use-throttle'; import { cn } from '@/utils/cn'; import { ChevronsUpDownIcon, type LucideIcon, SearchIcon } from 'lucide-react'; import { last } from 'ramda'; import { Children, useCallback, useEffect, useRef, useState } from 'react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu'; import { Input } from '../ui/input'; import type { WidgetHeadProps, WidgetTitleProps } from '../widget'; import { WidgetHead as WidgetHeadBase } from '../widget'; export function WidgetHead({ className, ...props }: WidgetHeadProps) { return ( ); } export function WidgetTitle({ children, className, icon: Icon, ...props }: WidgetTitleProps & { icon?: LucideIcon; }) { return (
{Icon && (
)} {children}
); } export function WidgetAbsoluteButtons({ className, children, ...props }: WidgetHeadProps) { return (
{children}
); } export function WidgetButtons({ className, children, ...props }: WidgetHeadProps) { const container = useRef(null); const sizes = useRef([]); const [slice, setSlice] = useState(3); // Show 3 buttons by default const gap = 16; const handleResize = useThrottle(() => { if (container.current) { if (sizes.current.length === 0) { // Get buttons const buttons: HTMLButtonElement[] = Array.from( container.current.querySelectorAll('button'), ); // Get sizes and cache them sizes.current = buttons.map( (button) => Math.ceil(button.offsetWidth) + gap, ); } const containerWidth = container.current.offsetWidth; const buttonsWidth = sizes.current.reduce((acc, size) => acc + size, 0); const moreWidth = (last(sizes.current) ?? 0) + gap; if (buttonsWidth > containerWidth) { const res = sizes.current.reduce( (acc, size, index) => { if (acc.size + size + moreWidth > containerWidth) { return { index: acc.index, size: acc.size + size }; } return { index, size: acc.size + size }; }, { index: 0, size: 0 }, ); setSlice(res.index); } else { setSlice(sizes.current.length - 1); } } }, 30); useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [handleResize, children]); const hidden = '!opacity-0 absolute pointer-events-none'; return (
{Children.map(children, (child, index) => { return ( ); })} {Children.map(children, (child, index) => { if (index <= slice) { return null; } return {child}; })}
); } interface WidgetTab { key: T; label: string; } interface WidgetHeadSearchableProps { tabs: WidgetTab[]; activeTab: T; className?: string; onTabChange: (key: T) => void; searchValue?: string; onSearchChange?: (value: string) => void; searchPlaceholder?: string; } export function WidgetHeadSearchable({ tabs, className, activeTab, onTabChange, searchValue, onSearchChange, searchPlaceholder = 'Search', }: WidgetHeadSearchableProps) { const scrollRef = useRef(null); const [showLeftGradient, setShowLeftGradient] = useState(false); const [showRightGradient, setShowRightGradient] = useState(false); const updateGradients = useCallback(() => { const el = scrollRef.current; if (!el) return; const { scrollLeft, scrollWidth, clientWidth } = el; const hasOverflow = scrollWidth > clientWidth; setShowLeftGradient(hasOverflow && scrollLeft > 0); setShowRightGradient( hasOverflow && scrollLeft < scrollWidth - clientWidth - 1, ); }, []); useEffect(() => { const el = scrollRef.current; if (!el) return; updateGradients(); el.addEventListener('scroll', updateGradients); window.addEventListener('resize', updateGradients); return () => { el.removeEventListener('scroll', updateGradients); window.removeEventListener('resize', updateGradients); }; }, [updateGradients]); // Update gradients when tabs change useEffect(() => { // Use RAF to ensure DOM has updated requestAnimationFrame(updateGradients); }, [tabs, updateGradients]); return (
{/* Scrollable tabs container */}
{/* Left gradient */}
{/* Scrollable tabs */}
{tabs.map((tab) => ( ))}
{/* Right gradient */}
{/* Search input */} {onSearchChange && (
onSearchChange(e.target.value)} className="pl-9 bg-transparent border-0 text-sm rounded-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground focus-visible:ring-offset-0 border-y" />
)}
); } export function WidgetFooter({ className, children, ...props }: WidgetHeadProps) { return (
{children}
); }