"use client" import { Menu } from 'lucide-react'; import * as React from 'react'; import * as TabsPrimitive from '@radix-ui/react-tabs'; import { useIsMobile } from '../../../hooks'; import { useStoredValue, type StorageType } from '../../../hooks'; import { useAppT } from '@djangocfg/i18n'; import { cn } from '../../../lib/utils'; import { Button } from '../../forms/button'; import { ScrollArea, ScrollBar } from '../../layout/scroll-area'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../overlay/sheet'; // ───────────────────────────────────────────────────────────────────────── // Tabs Root with Mobile Sheet Support // ───────────────────────────────────────────────────────────────────────── export interface TabsProps extends React.ComponentPropsWithoutRef { /** * Enable mobile sheet mode (automatically shows Sheet on mobile devices) * @default false */ mobileSheet?: boolean /** * Title for mobile sheet header * @default "Navigation" */ mobileSheetTitle?: string /** * Title to display next to burger menu on mobile */ mobileTitleText?: string /** * Enable sticky positioning (stays at top on scroll) * @default false */ sticky?: boolean /** * When provided, the active tab value is persisted under this key. * On mount the last active tab is restored automatically. * Works with both controlled (`value`) and uncontrolled (`defaultValue`) modes. * In controlled mode the stored value is passed to `defaultValue` on first * render; the parent stays in charge after that. * @example storageKey="admin-tab" */ storageKey?: string /** @default 'local' */ storageType?: StorageType } const Tabs = React.forwardRef< React.ElementRef, TabsProps >(({ mobileSheet = false, mobileSheetTitle, mobileTitleText, sticky = false, storageKey, storageType, children, ...props }, ref) => { const t = useAppT() const resolvedMobileSheetTitle = mobileSheetTitle ?? t('ui.navigation.title') const isMobile = useIsMobile() const [open, setOpen] = React.useState(false) const [storedTab, setStoredTab] = useStoredValue( storageKey, // seed: prefer defaultValue prop, then controlled value, then empty (props.defaultValue as string | undefined) ?? (props.value as string | undefined) ?? '', storageKey ? { storage: storageType ?? 'local' } : undefined, ); // Wrap onValueChange to persist tab changes const handleValueChange = React.useCallback( (newValue: string) => { if (storageKey) setStoredTab(newValue); props.onValueChange?.(newValue); }, [storageKey, setStoredTab, props.onValueChange], ); // Build enhanced props: inject stored defaultValue when no value/defaultValue given const tabsProps = { ...props, onValueChange: storageKey ? handleValueChange : props.onValueChange, // Only inject defaultValue when: storageKey set, no controlled value, storedTab non-empty ...(storageKey && props.value === undefined && storedTab ? { defaultValue: storedTab } : {}), }; // If mobile sheet mode is disabled, render normal tabs if (!mobileSheet || !isMobile) { // If sticky is enabled for desktop, wrap TabsList in sticky container if (sticky) { const childrenArray = React.Children.toArray(children) const tabsList = childrenArray.find( (child) => React.isValidElement(child) && child.type === TabsList ) const tabsContent = childrenArray.filter( (child) => React.isValidElement(child) && child.type === TabsContent ) return (
{tabsList}
{tabsContent}
) } return ( {children} ) } // Extract TabsList and TabsContent from children const childrenArray = React.Children.toArray(children) const tabsList = childrenArray.find( (child) => React.isValidElement(child) && child.type === TabsList ) const tabsContent = childrenArray.filter( (child) => React.isValidElement(child) && child.type === TabsContent ) // Extract triggers from TabsList children const triggers = React.isValidElement(tabsList) ? React.Children.toArray((tabsList.props as any).children) : [] // Mobile Sheet Navigation return (
{/* Title */} {mobileTitleText && (

{mobileTitleText}

)} {/* Menu Button */} {resolvedMobileSheetTitle}
{tabsContent}
) }) Tabs.displayName = "Tabs" // ───────────────────────────────────────────────────────────────────────── // Tabs variant context — lets TabsTrigger inherit variant from TabsList // ───────────────────────────────────────────────────────────────────────── type TabsVariant = 'default' | 'underline' const TabsVariantContext = React.createContext('default') const TabsList = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { /** * Visual variant. * - `default` — pill/card style (bg-muted container) * - `underline` — borderless, underline indicator on active tab * @default 'default' */ variant?: TabsVariant /** * Full width mode - tabs will stretch equally to fill the container * @default false */ fullWidth?: boolean /** * Enable horizontal scrolling when tabs overflow * @default false */ scrollable?: boolean } >(({ className, variant = 'default', fullWidth = false, scrollable = false, ...props }, ref) => { const isUnderline = variant === 'underline' const listCls = cn( isUnderline ? "inline-flex items-end gap-0 bg-transparent p-0 h-auto rounded-none" : "inline-flex h-9 items-center justify-center rounded-[var(--radius)] bg-muted p-1 text-muted-foreground", fullWidth && "w-full flex", className ) const list = ( ) // For underline variant — native overflow-x-auto, full-width border via absolute element const wrapped = isUnderline ? (
{list}
) : scrollable ? ( {list} ) : list return ( {wrapped} ) }) TabsList.displayName = TabsPrimitive.List.displayName const TabsTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { /** * Makes the trigger take equal space (use with fullWidth TabsList) * @default false */ flexEqual?: boolean key?: React.Key } >(({ className, flexEqual = false, ...props }, ref) => { const variant = React.useContext(TabsVariantContext) const isUnderline = variant === 'underline' return ( ) }) TabsTrigger.displayName = TabsPrimitive.Trigger.displayName const TabsContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { key?: React.Key } >(({ className, ...props }, ref) => ( )) TabsContent.displayName = TabsPrimitive.Content.displayName export { Tabs, TabsList, TabsTrigger, TabsContent }