import { motion } from 'framer-motion' import { useCallback, useRef, useState, type ReactNode } from 'react' import { useIsomorphicLayoutEffect } from '../hooks/useIsomorphicLayoutEffect' import { cn } from '../lib/cn' import { useTableStore } from './store' /** A filled, rounded-corner triangle used for the tab-strip step arrows. */ function TabArrow({ dir }: { dir: 'left' | 'right' }) { return ( ) } export type TableTabStripProps = { /** Stretchy middle region of the strip (e.g. filter badges). */ centerContent?: ReactNode /** Right-aligned region of the strip (e.g. actions + column picker). */ endContent?: ReactNode } /** * The folder-tab strip: the tab buttons, the sliding active-tab indicator * (`layoutId`-driven), and the Excel-style overflow step arrows. Reads the tab * set + active id from the store; chrome slots are supplied by the caller so * they can be reordered or omitted. */ export function TableTabStrip({ centerContent, endContent }: TableTabStripProps) { const { tabs, activeId, selectTab, indicatorLayoutId, classNames } = useTableStore() // ----- Horizontal tab-strip scrolling (Excel-style step arrows) ----- const tabListRef = useRef(null) const activeTabRef = useRef(null) const [scrollState, setScrollState] = useState({ overflow: false, atStart: true, atEnd: true, }) const syncScrollState = useCallback(() => { const c = tabListRef.current if (!c) return const max = c.scrollWidth - c.clientWidth setScrollState((prev) => { const next = { overflow: max > 1, atStart: c.scrollLeft <= 1, atEnd: c.scrollLeft >= max - 1, } return prev.overflow === next.overflow && prev.atStart === next.atStart && prev.atEnd === next.atEnd ? prev : next }) }, []) // Keep the active tab in view when the active id changes (click or // programmatic), and recompute the arrow state on scroll/resize. useIsomorphicLayoutEffect(() => { const container = tabListRef.current const btn = activeTabRef.current if (!container || !btn) return const cRect = container.getBoundingClientRect() const bRect = btn.getBoundingClientRect() if (bRect.left < cRect.left) { container.scrollLeft -= cRect.left - bRect.left } else if (bRect.right > cRect.right) { container.scrollLeft += bRect.right - cRect.right } }, [activeId]) useIsomorphicLayoutEffect(() => { const c = tabListRef.current if (!c) return syncScrollState() const onScroll = () => syncScrollState() c.addEventListener('scroll', onScroll, { passive: true }) let ro: ResizeObserver | undefined if (typeof ResizeObserver !== 'undefined') { ro = new ResizeObserver(() => syncScrollState()) ro.observe(c) } return () => { c.removeEventListener('scroll', onScroll) ro?.disconnect() } }, [syncScrollState]) // The tab set changing alters scrollWidth without resizing the container. useIsomorphicLayoutEffect(() => { syncScrollState() }, [tabs.length, syncScrollState]) // Scroll one tab toward `dir` (-1 left, +1 right), revealing the next // partially-hidden tab — like Excel's sheet-tab arrows. const scrollByTab = useCallback((dir: number) => { const c = tabListRef.current if (!c) return const cRect = c.getBoundingClientRect() const btns = Array.from(c.querySelectorAll('button')) let target: number if (dir > 0) { const next = btns.find((b) => b.getBoundingClientRect().right > cRect.right + 1) target = next ? c.scrollLeft + (next.getBoundingClientRect().right - cRect.right) : c.scrollWidth } else { const before = btns.filter((b) => b.getBoundingClientRect().left < cRect.left - 1) const prev = before[before.length - 1] target = prev ? c.scrollLeft - (cRect.left - prev.getBoundingClientRect().left) : 0 } c.scrollTo({ left: target, behavior: 'smooth' }) }, []) return (
{scrollState.overflow && ( )}
{tabs.map((tab) => { const isActive = tab.id === activeId return ( ) })}
{scrollState.overflow && ( )}
{/* --- Middle slot (e.g. filter badges); keeps its space so the tab list scrolls instead of squeezing it out --- */}
{centerContent}
{endContent && (
{endContent}
)}
) }