import { useCallback, useRef, type ComponentProps, type ComponentPropsWithRef, type CSSProperties, type ReactNode, } from 'react'; import type { PanelOnCollapse, PanelOnExpand } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels'; import { useVirtualizer } from '@tanstack/react-virtual'; import { cn } from '@wener/console'; import { HeaderContentFooterLayout } from '@wener/console/components'; import { ActionIcon } from '../icons/ActionIcon'; import { LeftContentRightLayout } from '../LeftContentRightLayout'; import { PanelResizeLineHandle } from '../ResizablePanel'; import { Tabs } from '../Tabs'; import { MeasureSize } from './MeasureSize'; type PanelConfig = { collapsedSize?: number | undefined; collapsible?: boolean | undefined; defaultSize?: number | undefined; id?: string; maxSize?: number | undefined; minSize?: number | undefined; onCollapse?: PanelOnCollapse; onExpand?: PanelOnExpand; }; const _rightConfig: PanelConfig = { defaultSize: 30, minSize: 20, maxSize: 60, collapsible: true, }; const _leftConfig: PanelConfig = { defaultSize: 15, minSize: 10, maxSize: 35, collapsible: true, }; export namespace DataViewLayout { type CompositeProps = ComponentPropsWithRef<'div'> & { header?: ReactNode; footer?: ReactNode; children?: ReactNode; left?: ReactNode; leftPanel?: ReactNode; right?: ReactNode; rightPanel?: ReactNode; }; export const Composite = ({ header, footer, leftPanel, children, rightPanel, left, right, ...props }: CompositeProps) => { /* ┌─────────────────────────────────────────────────────────┐ │ Header │ ├──────────┬─────────────────────────┬────────────────────┤ │ │ │ │ │ Left │ Content │ Right │ │ Panel │ ┌────────┐ │ Panel │ │ │ │Children│ │ │ │(optional)│ └────────┘ │ (optional) │ │ │ ┌────────┐ │ │ │ │ │ Footer │ │ │ │ │ └────────┘ │ │ └──────────┴─────────────────────────┴────────────────────┘ */ const hasLeftPanel = Boolean(left || leftPanel); const hasRightPanel = Boolean(right || rightPanel); return ( {hasLeftPanel && (left ?? {leftPanel})} {children} {hasRightPanel && (right ?? {rightPanel})} ); }; export const LeftPanel = ({ children }: { children?: ReactNode }) => { return ( <> {children} > ); }; export const RightPanel = ({ children, ...props }: ComponentProps) => { return ( <> {children} > ); }; export type HeaderProps = Omit, 'title'> & { prefix?: ReactNode; suffix?: ReactNode; title?: ReactNode; filter?: ReactNode; actions?: ReactNode; loading?: boolean; }; export const Header = ({ loading, children, className, prefix, suffix, title, filter, actions, ...props }: HeaderProps) => { /* ┌────────────────────────────────────────────────────────┐ │ [prefix] Title [filter] [actions] [suffix] │ │ │ │ └── flex-1 spacer ──┘ │ └────────────────────────────────────────────────────────┘ */ const parts: ReactNode[] = [prefix]; if (title) { parts.push( typeof title === 'string' ? ( {title} ) : ( title ), ); } parts.push(filter); // Actions and suffix typically go to the right if (actions || suffix) { parts.push(); parts.push(actions, suffix); } return ( {parts.filter(Boolean)} {children} ); }; export type FooterProps = ComponentPropsWithRef<'div'> & { left?: ReactNode; right?: ReactNode; }; export const Footer = ({ children, className, ...props }: FooterProps) => { /* ┌────────────────────────────────────────────────────────┐ │ [children - flex layout with gap-2] │ │ │ │ Typical usage: │ │ │ └────────────────────────────────────────────────────────┘ */ return ; }; type SidebarProps = ComponentPropsWithRef<'aside'> & { title?: ReactNode; header?: ReactNode; footer?: ReactNode; }; export const Sidebar = ({ title, header, footer, children, className, ...props }: SidebarProps) => { /* ┌─────────────────────────────┐ │ Title/Header │ ← header (fixed) ├─────────────────────────────┤ │ │ │ Children │ ← children (scrollable) │ (scrollable content) │ │ │ ├─────────────────────────────┤ │ Footer │ ← footer (fixed, optional) └─────────────────────────────┘ */ return ( ); }; export type SummaryProps = Omit, 'title'> & { title?: ReactNode; description?: ReactNode; tabs?: Tabs.TabItem[]; activeTab?: string; onTabChange?: (key: string) => void; header?: ReactNode; footer?: ReactNode; children?: ReactNode; onClose?: () => void; }; export const Summary = ({ title, description, tabs, activeTab, onTabChange, header, footer, children, className, onClose, ...props }: SummaryProps) => { /* ┌─────────────────────────────┐ │ Title [×]│ ← Header (title, description, close button) │ Description │ ├─────────────────────────────┤ │ Children Content │ ← children (fixed, optional, always before tabs) ├─────────────────────────────┤ │ Tab1 | Tab2 | Tab3 │ ← Tabs (optional, uses Tabs.Composite) ├─────────────────────────────┤ │ │ │ Tab Content │ ← Active tab content (scrollable, only if tabs exist) │ (scrollable) │ │ │ ├─────────────────────────────┤ │ Footer │ ← Footer (fixed, optional) └─────────────────────────────┘ */ return ( ); }; export type ListItemProps = Omit, 'title' | 'prefix'> & { selected?: boolean; onSelectedChange?: (selected: boolean) => void; header?: ReactNode; title?: ReactNode; prefix?: ReactNode; suffix?: ReactNode; trailing?: ReactNode; description?: ReactNode; onTitleClick?: () => void; actions?: ReactNode; meta?: ReactNode; children?: ReactNode; }; export const ListItem = ({ selected, onSelectedChange, header, title, prefix, suffix, trailing, description, onTitleClick, actions, meta, children, className, ...props }: ListItemProps) => { /* ┌─────────────────────────────────────────────────────────────┐ │ [✓] [header] | title [suffix] [trailing] │ │ description │ │ Actions · Meta: Created by Jane • 1 day ago │ │ children │ └─────────────────────────────────────────────────────────────┘ */ return ( {onSelectedChange && ( onSelectedChange?.(e.target.checked)} /> )} {header} {(title || suffix || trailing || prefix) && ( {prefix} {title && ( {title} )} {suffix} {trailing && {trailing}} )} {description} {(actions || meta) && ( {actions ? {actions} : } {meta && {meta}} )} {children} ); }; export type ListProps = ComponentPropsWithRef<'div'> & { data: T[]; renderItem: (props: { item: T; index: number; style: CSSProperties }) => ReactNode; estimatedItemSize?: number; overscan?: number; onScroll?: (event: React.UIEvent) => void; }; export const List = ({ data, renderItem, estimatedItemSize = 80, overscan = 5, className, onScroll, ...props }: ListProps) => { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: data.length, getScrollElement: () => parentRef.current, estimateSize: () => estimatedItemSize, overscan, // measureElement: // typeof window !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1 // ? (element) => element.getBoundingClientRect().height // : undefined, }); return ( virtualizer.measure, [virtualizer])}> {virtualizer.getVirtualItems().map((virtualItem) => { const item = data[virtualItem.index]; return ( {renderItem({ item, index: virtualItem.index, style: {} })} ); })} ); }; export type SummaryTabContentProps = ComponentPropsWithRef<'div'> & { header?: ReactNode; title?: ReactNode; footer?: ReactNode; footerActions?: ReactNode; footerInfo?: ReactNode; children?: ReactNode; }; export const SummaryTab = ({ header, title, footer, footerActions, footerInfo, children, className, ...props }: SummaryTabContentProps) => { /* ┌─────────────────────────────────────────┐ │ header | [title] │ ← Header 区域 ├─────────────────────────────────────────┤ │ │ │ children │ ← Content 区域 │ │ ├─────────────────────────────────────────┤ │ [footer] actions flex-1 info │ ← Footer 区域 └─────────────────────────────────────────┘ */ if (!header && !footer && !title && !footerActions && !footerInfo) { return ( {children} ); } // 构建 footer 内容 const footerContent = footer || footerActions || footerInfo ? ( {footer ? ( footer ) : ( {footerActions && {footerActions}} {footerInfo && {footerInfo}} )} ) : null; return ( {header} {title && {title}} ) } footer={footerContent} className={className} {...props} > {children} ); }; }