diff options
Diffstat (limited to 'components/client-table-v2/client-table-column-header.tsx')
| -rw-r--r-- | components/client-table-v2/client-table-column-header.tsx | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/components/client-table-v2/client-table-column-header.tsx b/components/client-table-v2/client-table-column-header.tsx new file mode 100644 index 00000000..2d8e5bce --- /dev/null +++ b/components/client-table-v2/client-table-column-header.tsx @@ -0,0 +1,235 @@ +"use client" + +import * as React from "react" +import { Header, Column } from "@tanstack/react-table" +import { useSortable } from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import { flexRender } from "@tanstack/react-table" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { + ArrowDown, + ArrowUp, + ChevronsUpDown, + EyeOff, + PinOff, + MoveLeft, + MoveRight, + Group, + Ungroup, +} from "lucide-react" +import { cn } from "@/lib/utils" +import { ClientTableFilter } from "../client-table/client-table-filter" + +interface ClientTableColumnHeaderProps<TData, TValue> + extends React.HTMLAttributes<HTMLTableHeaderCellElement> { + header: Header<TData, TValue> + enableReordering?: boolean + renderHeaderVisualFeedback?: (props: { + column: Column<TData, TValue> + isPinned: boolean | string + isSorted: boolean | string + isFiltered: boolean + isGrouped: boolean + }) => React.ReactNode +} + +export function ClientTableColumnHeader<TData, TValue>({ + header, + enableReordering = true, + renderHeaderVisualFeedback, + className, + ...props +}: ClientTableColumnHeaderProps<TData, TValue>) { + const column = header.column + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + id: header.id, + disabled: !enableReordering || column.getIsResizing(), + }) + + // -- Styles -- + const style: React.CSSProperties = { + // Apply transform only if reordering is enabled and active + transform: enableReordering ? CSS.Translate.toString(transform) : undefined, + transition: enableReordering ? transition : undefined, + width: header.getSize(), + zIndex: isDragging ? 100 : 0, + position: "relative", + ...props.style, + } + + // Pinning Styles + const isPinned = column.getIsPinned() + const isSorted = column.getIsSorted() + const isFiltered = column.getFilterValue() !== undefined + const isGrouped = column.getIsGrouped() + + if (isPinned === "left") { + style.left = `${column.getStart("left")}px` + style.position = "sticky" + style.zIndex = 30 // Pinned columns needs to be higher than normal headers + } else if (isPinned === "right") { + style.right = `${column.getAfter("right")}px` + style.position = "sticky" + style.zIndex = 30 // Pinned columns needs to be higher than normal headers + } + + // -- Handlers -- + const handleHide = () => column.toggleVisibility(false) + const handlePinLeft = () => column.pin("left") + const handlePinRight = () => column.pin("right") + const handleUnpin = () => column.pin(false) + const handleToggleGrouping = () => column.toggleGrouping() + + // -- Content -- + const content = ( + <> + <div + className={cn( + "flex items-center gap-2", + column.getCanSort() ? "cursor-pointer select-none" : "" + )} + onClick={column.getToggleSortingHandler()} + > + {flexRender(column.columnDef.header, header.getContext())} + {column.getCanSort() && ( + <span className="flex items-center"> + {column.getIsSorted() === "desc" ? ( + <ArrowDown className="h-4 w-4" /> + ) : column.getIsSorted() === "asc" ? ( + <ArrowUp className="h-4 w-4" /> + ) : ( + <ChevronsUpDown className="h-4 w-4 opacity-50" /> + )} + </span> + )} + {isGrouped && <Group className="h-4 w-4 text-blue-500" />} + </div> + + {/* Resize Handle */} + <div + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + onPointerDown={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} // Prevent sort trigger + className={cn( + "absolute right-0 top-0 h-full w-2 cursor-col-resize select-none touch-none z-10", + "after:absolute after:right-0 after:top-0 after:h-full after:w-[1px] after:bg-border", // 시각적 구분선 + "hover:bg-primary/20 hover:w-4 hover:-right-2", // 호버 시 클릭 영역 확장 + header.column.getIsResizing() ? "bg-primary/50 w-1" : "bg-transparent" + )} + /> + + {/* Filter */} + {column.getCanFilter() && <ClientTableFilter column={column} />} + + {/* Visual Feedback Indicators */} + {renderHeaderVisualFeedback ? ( + renderHeaderVisualFeedback({ + column, + isPinned, + isSorted, + isFiltered, + isGrouped, + }) + ) : ( + (isPinned || isFiltered || isGrouped) && ( + <div className="absolute top-0.5 right-1 flex gap-1 z-10 pointer-events-none"> + {isPinned && <div className="h-1.5 w-1.5 rounded-full bg-blue-500" />} + {isFiltered && <div className="h-1.5 w-1.5 rounded-full bg-yellow-500" />} + {isGrouped && <div className="h-1.5 w-1.5 rounded-full bg-green-500" />} + </div> + ) + )} + </> + ) + + if (header.isPlaceholder) { + return ( + <th + colSpan={header.colSpan} + style={style} + className={cn("border-b px-4 py-2 text-left text-sm font-medium bg-muted", className)} + {...props} + > + {null} + </th> + ) + } + + return ( + <ContextMenu> + <ContextMenuTrigger asChild> + <th + ref={setNodeRef} + colSpan={header.colSpan} + style={style} + className={cn( + "border-b px-4 py-2 text-left text-sm font-medium bg-muted group transition-colors", + isDragging ? "opacity-50 bg-accent" : "", + isPinned ? "shadow-[0_0_10px_rgba(0,0,0,0.1)]" : "", + className + )} + {...attributes} + {...listeners} + {...props} + > + {content} + </th> + </ContextMenuTrigger> + <ContextMenuContent className="w-48"> + <ContextMenuItem onClick={handleHide}> + <EyeOff className="mr-2 h-4 w-4" /> + Hide Column + </ContextMenuItem> + + {column.getCanGroup() && ( + <> + <ContextMenuSeparator /> + <ContextMenuItem onClick={handleToggleGrouping}> + {isGrouped ? ( + <> + <Ungroup className="mr-2 h-4 w-4" /> + Ungroup + </> + ) : ( + <> + <Group className="mr-2 h-4 w-4" /> + Group by {column.id} + </> + )} + </ContextMenuItem> + </> + )} + + <ContextMenuSeparator /> + <ContextMenuItem onClick={handlePinLeft}> + <MoveLeft className="mr-2 h-4 w-4" /> + Pin Left + </ContextMenuItem> + <ContextMenuItem onClick={handlePinRight}> + <MoveRight className="mr-2 h-4 w-4" /> + Pin Right + </ContextMenuItem> + {isPinned && ( + <ContextMenuItem onClick={handleUnpin}> + <PinOff className="mr-2 h-4 w-4" /> + Unpin + </ContextMenuItem> + )} + </ContextMenuContent> + </ContextMenu> + ) +} |
