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 (
);
}
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 (
{child}
);
})}
{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}
);
}