UNPKG

@sineways/react-tablefront

Version:

React Data Table and Data Grid for TypeScript. Zero config with fast search and filters, pagination and infinite scroll, table grid and masonry layouts. Built on TanStack Table.

1,317 lines (1,301 loc) 225 kB
// src/DataTable.tsx import React11, { useEffect as useEffect14, useRef as useRef10, useCallback as useCallback12, useState as useState15, useMemo as useMemo9, startTransition } from "react"; // src/hooks/useWindowResize.ts import { useState, useEffect } from "react"; function useWindowResize() { const [windowSize, setWindowSize] = useState({ width: typeof window !== "undefined" ? window.innerWidth : 1200, height: typeof window !== "undefined" ? window.innerHeight : 800 }); useEffect(() => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); } window.addEventListener("resize", handleResize); handleResize(); return () => window.removeEventListener("resize", handleResize); }, []); return windowSize; } // src/constants/resize.ts var DEFAULT_RESIZE_DOUBLE_CLICK_DELAY = 150; var DEFAULT_RESIZE_RESET_DEBOUNCE = 50; // src/utils.ts import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { parseISO, isValid } from "date-fns"; import { toDate } from "date-fns-tz"; function cn(...inputs) { return twMerge(clsx(inputs)); } function parseDate(value, timeZone = "UTC") { if (!value) return void 0; if (value instanceof Date) return value; try { if (typeof value === "string" && value.includes("T")) { return toDate(parseISO(value), { timeZone }); } const date = new Date(value); return isValid(date) ? date : void 0; } catch { return void 0; } } var createDragState = () => ({ isDragging: false, draggedColumnId: null, dragStartX: 0, dragStartY: 0, currentX: 0, currentY: 0, dropTargetIndex: null, dropTargetColumnId: null }); var getColumnIdFromElement = (element) => { return element.getAttribute("data-column-id"); }; var setDragImage = (event, element) => { if (event.dataTransfer) { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setDragImage(element, 0, 0); } }; var getDropTargetIndex = (x, headerCells, draggedIndex) => { if (headerCells.length === 0) return { index: -1, columnId: null }; let left = 0; let right = headerCells.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); const cell = headerCells[mid]; const rect = cell.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; if (x < centerX) { right = mid - 1; } else { left = mid + 1; } } const targetIndex = left; const targetCell = headerCells[targetIndex] || headerCells[headerCells.length - 1]; return { index: targetIndex, columnId: getColumnIdFromElement(targetCell) }; }; var addDropIndicator = (targetElement, position, dropIndicatorClass = "absolute top-0 bottom-0 w-1 bg-primary z-10") => { const indicator = document.createElement("div"); indicator.className = dropIndicatorClass; if (position === "before") { indicator.style.left = "0"; } else { indicator.style.right = "0"; } targetElement.style.position = "relative"; targetElement.appendChild(indicator); return indicator; }; var removeDropIndicator = (indicator) => { if (indicator && indicator.parentNode) { indicator.parentNode.removeChild(indicator); } }; var createResizeState = () => ({ isResizing: false, columnId: null, startX: 0, startWidth: 0 }); var RESIZE_CONSTRAINTS = { MIN_WIDTH: 50, MAX_WIDTH: 8e3 }; var applyResizeCursor = (isResizing) => { if (typeof document === "undefined") return; if (isResizing) { document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; } else { document.body.style.cursor = ""; document.body.style.userSelect = ""; } }; var getColumnWidth = (element) => { return element.getBoundingClientRect().width; }; var applyColumnWidthToDomImmediate = (columnId, width) => { const clampedWidth = Math.max(RESIZE_CONSTRAINTS.MIN_WIDTH, Math.min(RESIZE_CONSTRAINTS.MAX_WIDTH, width)); const headerCell = document.querySelector(`th[data-column-id="${columnId}"]`); if (!headerCell) return clampedWidth; const table = headerCell.closest("table"); const columnIndex = Array.from(headerCell.parentElement?.children || []).indexOf(headerCell); applyColumnStyles(headerCell, clampedWidth); if (table && columnIndex >= 0) { const bodyCells = table.querySelectorAll(`tbody td:nth-child(${columnIndex + 1})`); bodyCells.forEach((cell) => { applyColumnStyles(cell, clampedWidth); }); } headerCell.offsetWidth; return clampedWidth; }; var applyColumnStyles = (element, width) => { element.style.width = `${width}px`; element.style.minWidth = `${width}px`; element.style.maxWidth = `${width}px`; }; var clearColumnStyles = (columnId) => { const headerCell = document.querySelector(`th[data-column-id="${columnId}"]`); if (!headerCell) return; const table = headerCell.closest("table"); const columnIndex = Array.from(headerCell.parentElement?.children || []).indexOf(headerCell); clearElementStyles(headerCell); if (table && columnIndex >= 0) { const bodyCells = table.querySelectorAll(`tbody td:nth-child(${columnIndex + 1})`); bodyCells.forEach((cell) => { clearElementStyles(cell); }); } headerCell.offsetWidth; }; var clearElementStyles = (element) => { element.style.width = ""; element.style.minWidth = ""; element.style.maxWidth = ""; }; var HEADER_MIN_WIDTH_FOR_TEXT = 40; var shouldShowHeaderText = (columnWidth) => { return columnWidth > HEADER_MIN_WIDTH_FOR_TEXT; }; var truncateHeaderText = (text, maxLength = 15) => { if (!text || text.length <= maxLength) return text; return text.substring(0, maxLength).trim() + "..."; }; var getCurrentColumnWidth = (columnId) => { const headerCell = document.querySelector(`th[data-column-id="${columnId}"]`); if (!headerCell) return 0; return headerCell.getBoundingClientRect().width; }; // src/variants.tsx import { useMemo } from "react"; // src/defaultStyles.ts var defaultTableStyles = { container: "flex-1 w-full min-h-0 flex flex-col focus:outline-none", searchBar: { wrapper: "px-0 pt-0 mb-2", containerWrapper: "relative", container: "pr-1.5 relative flex items-center w-full h-12 rounded-lg bg-foreground/5 hover:bg-foreground/10 focus-within:ring-1 focus-within:ring-primary transition-colors", icon: "absolute left-3 w-5 h-5 text-muted-foreground", input: "w-full bg-transparent border-none focus:outline-none text-sm pl-10 pr-2 py-1.5 placeholder:text-muted-foreground", clearButton: "", clearButtonIcon: "w-4 h-4" }, header: { container: "flex items-center justify-between py-1 pl-3 pr-1 border-b border-border shrink-0 bg-foreground/5 rounded-t-lg", leftSection: "flex items-center gap-2", resultCount: "text-xs font-medium text-muted-foreground", clearFiltersButton: "text-xs px-3", clearFiltersIcon: "size-5", rightSection: "flex items-center gap-1" }, table: { scrollArea: "w-full flex-1 min-h-0 overflow-auto", table: "w-full caption-bottom text-sm bg-foreground/2", tableHeader: "h-10 sticky top-0 z-20 bg-background border-b border-border shadow-sm", tableRow: "border-b h-10", tableRowSelected: "bg-primary/10 hover:bg-primary/10", tableRowHover: "hover:bg-foreground/5", tableCell: "px-4 align-middle", tableHeaderCell: "h-10 px-4 font-medium text-muted-foreground cursor-pointer select-none hover:bg-foreground/5", expandHeader: "min-w-10 w-10 px-0 text-center", expandButton: "size-1 px-0" }, grid: { container: "grid w-full h-full gap-1 py-1", item: "bg-card border border-border rounded-lg p-4 cursor-pointer transition-all duration-200", itemHover: "hover:bg-accent/50 hover:border-accent/50 hover:shadow-md", itemSelected: "bg-primary/10 border-primary/30 shadow-lg", itemContent: "space-y-3", expandButton: "w-8 h-8 flex items-center justify-center rounded hover:bg-foreground/5", itemFields: "space-y-2", field: "space-y-1", fieldLabel: "text-xs font-medium text-muted-foreground uppercase tracking-wide", fieldValue: "text-sm font-medium text-foreground", expandedContent: "mt-4 pt-4 border-t border-border" }, masonry: { container: "flex w-full h-full py-1 gap-1", column: "flex flex-col gap-1", item: "bg-card border border-border rounded-lg p-4 cursor-pointer transition-all duration-200", itemHover: "hover:bg-accent/50 hover:border-accent/50 hover:shadow-md", itemSelected: "bg-primary/10 border-primary/30 shadow-lg", itemContent: "space-y-3", expandButton: "w-8 h-8 flex items-center justify-center rounded hover:bg-foreground/5", itemFields: "space-y-2", field: "space-y-1", fieldLabel: "text-xs font-medium text-muted-foreground uppercase tracking-wide", fieldValue: "text-sm font-medium text-foreground", expandedContent: "mt-4 pt-4 border-t border-border" }, loadingState: { container: "flex items-center justify-center h-full w-full p-6 text-muted-foreground", content: "space-y-1 text-center", icon: "h-6 w-6 animate-spin", text: "text-sm" }, emptyState: { container: "flex items-center justify-center h-full w-full p-6 text-muted-foreground", content: "space-y-2 text-center", text: "text-sm" }, pagination: { variant: "version2", container: "flex items-center justify-center px-2 py-1 border-t border-border bg-background", info: "text-xs text-muted-foreground px-3", controls: "flex items-center gap-2", buttonGroup: "flex items-center gap-2", button: "h-8 w-8 p-0 hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed", buttonIcon: "h-4 w-4" }, columnVisibility: { trigger: "size-9", triggerIcon: "size-5", content: "w-72 p-4", header: "space-y-4", toggleAllButton: "text-sm font-medium cursor-pointer", itemList: "space-y-0", item: "flex items-center justify-between space-x-2", checkbox: "cursor-pointer" }, filterPopover: { trigger: "size-9", triggerIcon: "size-5", content: "max-w-[95vw] p-4 bg-card", container: "", header: "", title: "text-sm font-medium mb-3", grid: "grid grid-cols-1 sm:grid-cols-2 gap-3", column: "space-y-1.5", filterButton: "min-w-[170px] text-xs bg-foreground/5 hover:bg-foreground/10 cursor-pointer", filterButtonActive: "bg-primary/15 hover:bg-primary/20 text-primary hover:text-primary font-medium shadow-sm", filterButtonInactive: "bg-foreground/5 hover:bg-foreground/10", filterLabel: "text-xs whitespace-nowrap" }, dragDrop: { dragGhost: "pointer-events-none z-[9999] opacity-80 bg-background", dropIndicator: "absolute top-0 bottom-0 w-1 bg-primary z-10", dragTarget: "bg-muted/50", dragSource: "opacity-50" }, resize: { handle: "absolute top-0 right-0 bottom-0 w-1 group-hover:bg-primary/20 z-10", indicator: "absolute top-1/2 right-0 w-1 h-4 group-hover:bg-primary/40 transform -translate-y-1/2 rounded-full", overlay: "fixed inset-0 z-[9999] cursor-col-resize", hitslop: "absolute top-0 right-0 bottom-0 w-2 cursor-col-resize group z-10" } }; var modernTableStyles = { container: "flex-1 w-full min-h-0 flex flex-col focus:outline-none bg-background", searchBar: { wrapper: "px-0 pt-0 mb-1", containerWrapper: "relative", container: "relative flex items-center w-full h-12 rounded-xl bg-gradient-to-r from-card to-card/80 hover:from-accent/5 hover:to-accent/10 focus-within:ring-2 focus-within:ring-primary transition-all duration-300 shadow-inner border border-border/50", icon: "absolute left-3 w-5 h-5 text-primary", input: "w-full bg-transparent border-none focus:outline-none text-sm pl-10 pr-4 py-1.5 placeholder:text-muted-foreground transition-all duration-200", clearButton: "absolute right-2", clearButtonIcon: "w-4 h-4" }, header: { container: "flex items-center justify-between py-3 px-4 border-b border-border bg-gradient-to-r from-card/50 to-card/70 backdrop-blur-md shadow-sm", leftSection: "flex items-center gap-3", resultCount: "text-sm font-semibold text-foreground", clearFiltersButton: "", clearFiltersIcon: "size-5", rightSection: "flex items-center gap-1" }, table: { scrollArea: "w-full flex-1 min-h-0 bg-card/20 overflow-auto", table: "w-full text-sm", tableHeader: "sticky top-0 z-10 bg-gradient-to-b from-card/90 to-card/70 backdrop-blur-md border-b border-border shadow-md", tableRow: "border-b border-border/50 cursor-pointer transition-all duration-200", tableRowSelected: "bg-gradient-to-r from-primary/5 via-primary/15 to-primary/5 hover:from-primary/10 hover:via-primary/25 hover:to-primary/10 shadow-lg", tableRowHover: "hover:bg-gradient-to-r hover:from-accent/40 hover:via-accent/60 hover:to-accent/40", tableCell: "p-4 align-middle", tableHeaderCell: "h-12 px-4 text-left align-middle font-semibold text-card-foreground cursor-pointer select-none hover:bg-gradient-to-r hover:from-accent/40 hover:via-accent/60 hover:to-accent/40 transition-colors duration-200", expandHeader: "w-10 px-0 text-center", expandButton: "w-10 px-0" }, grid: { container: "grid gap-6 p-6", item: "bg-gradient-to-br from-card to-card/80 border border-border/50 rounded-xl p-6 cursor-pointer transition-all duration-300 shadow-lg hover:shadow-xl", itemHover: "hover:bg-gradient-to-br hover:from-accent/20 hover:via-accent/30 hover:to-accent/20 hover:border-accent/50 hover:scale-105", itemSelected: "bg-gradient-to-br from-primary/10 via-primary/20 to-primary/10 border-primary/50 shadow-2xl scale-105", itemContent: "space-y-4", expandButton: "w-10 h-10 flex items-center justify-center rounded-lg hover:bg-foreground/10 transition-all duration-200", itemFields: "space-y-3", field: "space-y-1.5", fieldLabel: "text-xs font-semibold text-primary uppercase tracking-wider", fieldValue: "text-sm font-semibold text-foreground", expandedContent: "mt-6 pt-6 border-t border-border/50" }, masonry: { container: "flex gap-6 p-6", column: "flex flex-col gap-6", item: "bg-gradient-to-br from-card to-card/80 border border-border/50 rounded-xl p-6 cursor-pointer transition-all duration-300 shadow-lg hover:shadow-xl", itemHover: "hover:bg-gradient-to-br hover:from-accent/20 hover:via-accent/30 hover:to-accent/20 hover:border-accent/50 hover:scale-105", itemSelected: "bg-gradient-to-br from-primary/10 via-primary/20 to-primary/10 border-primary/50 shadow-2xl scale-105", itemContent: "space-y-4", expandButton: "w-10 h-10 flex items-center justify-center rounded-lg hover:bg-foreground/10 transition-all duration-200", itemFields: "space-y-3", field: "space-y-1.5", fieldLabel: "text-xs font-semibold text-primary uppercase tracking-wider", fieldValue: "text-sm font-semibold text-foreground", expandedContent: "mt-6 pt-6 border-t border-border/50" }, loadingState: { container: "flex items-center justify-center h-full w-full p-6 bg-gradient-to-br from-background to-background/80 text-muted-foreground", content: "space-y-1 text-center", icon: "h-6 w-6 animate-spin text-primary", text: "text-sm" }, emptyState: { container: "flex items-center justify-center h-full w-full p-6 bg-gradient-to-br from-background to-background/80 text-muted-foreground", content: "space-y-2 text-center", text: "text-sm" }, pagination: { container: "flex items-center justify-center px-4 py-2 border-t border-border bg-gradient-to-r from-background to-background/90", info: "text-xs text-muted-foreground px-3", controls: "flex items-center gap-2", buttonGroup: "flex items-center gap-2", button: "h-8 w-8 p-0 hover:bg-gradient-to-r hover:from-accent/20 hover:via-accent/30 hover:to-accent/20 disabled:opacity-50 disabled:cursor-not-allowed rounded-full", buttonIcon: "h-4 w-4" }, columnVisibility: { trigger: "h-10 w-10", triggerIcon: "size-5 text-primary", content: "w-80 p-6 bg-gradient-to-br from-card to-card/80 shadow-lg rounded-xl border border-border/50 backdrop-blur-md", header: "space-y-4", toggleAllButton: "text-sm font-medium cursor-pointer", itemList: "space-y-1 pr-2", item: "flex items-center justify-between space-x-2", checkbox: "cursor-pointer" }, filterPopover: { trigger: "h-10 w-10", triggerIcon: "size-5 text-primary", content: "max-w-[95vw] p-6 bg-gradient-to-br from-card to-card/80 shadow-lg rounded-xl border border-border/50 backdrop-blur-md", container: "p-0", header: "bg-gradient-to-r from-card/50 to-card/70 backdrop-blur-md rounded-t-xl p-3", title: "text-sm font-semibold mb-3 text-foreground", grid: "grid grid-cols-1 sm:grid-cols-2 gap-4", column: "space-y-2", filterButton: "min-w-[150px] rounded-xl py-2.5 px-3 text-center text-xs transition-all duration-200 shadow-sm hover:scale-105", filterButtonActive: "bg-gradient-to-r from-primary/15 via-primary/25 to-primary/15 text-primary font-semibold shadow-md border border-primary/30 hover:shadow-xl hover:scale-105", filterButtonInactive: "bg-gradient-to-r from-accent/40 via-accent/50 to-accent/40 hover:from-accent/50 hover:via-accent/60 hover:to-accent/50 text-foreground", filterLabel: "text-xs whitespace-nowrap text-muted-foreground font-medium" }, dragDrop: { dragGhost: "pointer-events-none z-[9999] opacity-80 bg-background/50", dropIndicator: "absolute top-0 bottom-0 w-1 bg-primary/50 z-10", dragTarget: "bg-accent/50", dragSource: "opacity-50" }, resize: { handle: "absolute top-0 right-0 bottom-0 w-1 group-hover:bg-primary/20 z-10", indicator: "absolute top-1/2 right-0 w-1 h-4 bg-border group-hover:bg-primary/40 transform -translate-y-1/2 rounded-full", overlay: "fixed inset-0 z-[9999] cursor-col-resize", hitslop: "absolute top-0 right-0 bottom-0 w-2 cursor-col-resize group z-10" } }; var compactTableStyles = { container: "flex-1 w-full min-h-0 flex flex-col focus:outline-none", searchBar: { wrapper: "px-0 pt-0 mb-1", containerWrapper: "relative", container: "relative flex items-center w-full h-8 rounded bg-muted hover:bg-accent focus-within:ring-1 focus-within:ring-primary transition-colors", icon: "absolute left-2 w-3.5 h-3.5 text-muted-foreground", input: "w-full bg-transparent border-none focus:outline-none text-xs pl-7 pr-3 py-1 placeholder:text-muted-foreground", clearButton: "absolute right-1", clearButtonIcon: "w-3 h-3" }, header: { container: "flex items-center justify-between py-1 px-2 border-b border-border bg-muted/30 min-h-8", leftSection: "flex items-center gap-1", resultCount: "text-xs text-muted-foreground", clearFiltersButton: "", clearFiltersIcon: "size-4", rightSection: "flex items-center gap-1" }, table: { scrollArea: "w-full flex-1 min-h-0 overflow-auto", table: "w-full text-xs", tableHeader: "sticky top-0 z-10 bg-muted/50 border-b border-border backdrop-blur-md", tableRow: "border-b transition-colors cursor-pointer", tableRowSelected: "bg-primary/10 hover:bg-primary/15", tableRowHover: "hover:bg-muted/30", tableCell: "p-2 align-middle text-xs", tableHeaderCell: "h-8 px-2 text-left align-middle font-medium text-muted-foreground cursor-pointer select-none hover:bg-muted/50 text-xs", expandHeader: "w-6 px-0 text-center", expandButton: "w-6 px-0" }, grid: { container: "grid gap-2 p-2", item: "bg-card border border-border rounded p-2 cursor-pointer transition-colors", itemHover: "hover:bg-accent/30 hover:border-accent/50", itemSelected: "bg-primary/10 border-primary/30 shadow-sm", itemContent: "space-y-1.5", expandButton: "w-6 h-6 flex items-center justify-center rounded hover:bg-foreground/5", itemFields: "space-y-1", field: "space-y-0.5", fieldLabel: "text-xs font-medium text-muted-foreground", fieldValue: "text-xs font-medium text-foreground", expandedContent: "mt-2 pt-2 border-t border-border" }, masonry: { container: "flex gap-2 p-2", column: "flex flex-col gap-2", item: "bg-card border border-border rounded p-2 cursor-pointer transition-colors", itemHover: "hover:bg-accent/30 hover:border-accent/50", itemSelected: "bg-primary/10 border-primary/30 shadow-sm", itemContent: "space-y-1.5", expandButton: "w-6 h-6 flex items-center justify-center rounded hover:bg-foreground/5", itemFields: "space-y-1", field: "space-y-0.5", fieldLabel: "text-xs font-medium text-muted-foreground", fieldValue: "text-xs font-medium text-foreground", expandedContent: "mt-2 pt-2 border-t border-border" }, loadingState: { container: "flex items-center justify-center h-full w-full p-4 text-muted-foreground", content: "space-y-1 text-center", icon: "h-4 w-4 animate-spin", text: "text-xs" }, emptyState: { container: "flex items-center justify-center h-full w-full p-4 text-muted-foreground", content: "space-y-1 text-center", text: "text-xs" }, pagination: { container: "flex items-center justify-center px-2 py-1 border-t border-border bg-background", info: "text-xs text-muted-foreground px-2", controls: "flex items-center gap-1", buttonGroup: "flex items-center gap-1", button: "h-6 w-6 p-0 hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed", buttonIcon: "h-3 w-3" }, columnVisibility: { trigger: "h-8 w-8", triggerIcon: "size-4", content: "min-w-64 p-4", header: "space-y-4", toggleAllButton: "text-sm font-medium cursor-pointer", itemList: "space-y-1 pr-2", item: "flex items-center justify-between space-x-2", checkbox: "cursor-pointer" }, filterPopover: { trigger: "h-8 w-8", triggerIcon: "size-4", content: "max-w-[95vw] p-4", container: "p-0", header: "bg-muted/30 rounded p-1.5", title: "text-xs font-medium mb-2 px-1", grid: "grid grid-cols-1 sm:grid-cols-2 gap-2 px-1 pb-1", column: "space-y-1", filterButton: "min-w-[120px] rounded py-1.5 px-2 text-center text-xs transition-colors", filterButtonActive: "bg-primary/15 text-primary font-medium", filterButtonInactive: "bg-muted/50 hover:bg-muted/70 text-foreground", filterLabel: "text-xs whitespace-nowrap text-muted-foreground" }, dragDrop: { dragGhost: "pointer-events-none z-[9999] opacity-80 bg-background", dropIndicator: "absolute top-0 bottom-0 w-0.5 bg-primary z-10", dragTarget: "bg-muted/50", dragSource: "opacity-50" }, resize: { handle: "absolute top-0 right-0 bottom-0 w-1 group-hover:bg-primary/20 z-10", indicator: "absolute top-1/2 right-0 w-1 h-4 bg-border group-hover:bg-primary/40 transform -translate-y-1/2 rounded-full", overlay: "fixed inset-0 z-[9999] cursor-col-resize", hitslop: "absolute top-0 right-0 bottom-0 w-2 cursor-col-resize group z-10" } }; var tableStylePresets = { default: defaultTableStyles, modern: modernTableStyles, compact: compactTableStyles }; function getTableStylePreset(preset) { return tableStylePresets[preset]; } // src/variants.tsx var baseStyles = { container: "flex-1 w-full min-h-0 flex flex-col", searchBar: { wrapper: "", containerWrapper: "", container: "", icon: "", input: "", clearButton: "", clearButtonIcon: "" }, header: { container: "", leftSection: "", resultCount: "", clearFiltersButton: "", clearFiltersIcon: "", rightSection: "" }, table: { scrollArea: "", table: "", tableHeader: "", tableRow: "", tableRowSelected: "", tableRowHover: "", tableCell: "", tableHeaderCell: "", expandHeader: "", expandButton: "" }, grid: { container: "", item: "", itemHover: "", itemSelected: "", itemContent: "", expandButton: "", itemFields: "", field: "", fieldLabel: "", fieldValue: "", expandedContent: "" }, masonry: { container: "", column: "", item: "", itemHover: "", itemSelected: "", itemContent: "", expandButton: "", itemFields: "", field: "", fieldLabel: "", fieldValue: "", expandedContent: "" }, loadingState: { container: "", content: "", icon: "", text: "" }, emptyState: { container: "", content: "", text: "" }, pagination: { variant: "version1", container: "", info: "", controls: "", buttonGroup: "", button: "", buttonIcon: "" }, columnVisibility: { trigger: "", triggerIcon: "", content: "", header: "", toggleAllButton: "", itemList: "", item: "", checkbox: "" }, filterPopover: { trigger: "", triggerIcon: "", content: "", container: "", header: "", title: "", grid: "", column: "", filterButton: "", filterButtonActive: "", filterButtonInactive: "", filterLabel: "" }, dragDrop: { dragGhost: "", dropIndicator: "", dragTarget: "", dragSource: "" }, resize: { handle: "", indicator: "", overlay: "", hitslop: "" } }; function mergeStyles(base, custom) { const result = { ...base }; Object.entries(custom).forEach(([key, value]) => { if (value === void 0) return; if (typeof value === "string") { ; result[key] = value; } else if (typeof value === "object" && value !== null) { const r = result; const existing = r[key] || {}; r[key] = { ...existing, ...value }; } }); return result; } function getTableStyles(customStyles) { return customStyles ? mergeStyles(baseStyles, customStyles) : mergeStyles(baseStyles, defaultTableStyles); } function useTableStyles(customStyles) { return useMemo(() => getTableStyles(customStyles), [customStyles]); } // src/components/LicenseEnforcer.tsx import { useEffect as useEffect3 } from "react"; // src/licensing.ts import { useEffect as useEffect2, useState as useState2 } from "react"; function useLicenseStatus() { const [status, setStatus] = useState2({ valid: false, ready: false }); useEffect2(() => { let cancelled = false; async function readStatus() { try { const envToken = typeof process !== "undefined" && process.env && (process.env.NEXT_PUBLIC_TABLEFRONT_VALIDATION_TOKEN || process.env.REACT_APP_TABLEFRONT_VALIDATION_TOKEN || process.env.VITE_TABLEFRONT_VALIDATION_TOKEN) || void 0; const envKey = typeof process !== "undefined" && process.env && (process.env.NEXT_PUBLIC_TABLEFRONT_VALIDATION_KEY || process.env.REACT_APP_TABLEFRONT_VALIDATION_KEY || process.env.VITE_TABLEFRONT_VALIDATION_KEY) || void 0; const token = envToken; const publicKeyB64u = envKey; if (!token || !publicKeyB64u) { if (!cancelled) setStatus({ valid: false, ready: true }); return; } const derivedValid = getJwtValidFlag(token); if (!cancelled) setStatus((s) => { if (s.valid === derivedValid && s.ready === true) return s; return { valid: derivedValid, ready: true }; }); } catch { if (!cancelled) setStatus({ valid: false, ready: true }); } } readStatus(); const onFocus = () => { }; const onVisibility = () => { }; return () => { cancelled = true; try { window.removeEventListener("focus", onFocus); } catch { } try { document.removeEventListener("visibilitychange", onVisibility); } catch { } }; }, []); return status; } function getJwtValidFlag(token) { try { if (!token) return false; const parts = token.split("."); if (parts.length !== 3) return false; const payloadB64u = parts[1]; const payloadJson = base64UrlToString(payloadB64u); const payload = JSON.parse(payloadJson); return !!(payload && payload.valid === true); } catch { return false; } } function base64UrlToString(b64u) { const s = base64UrlToBase64(b64u); return decodeURIComponent(escape(atob(s))); } function base64UrlToBase64(b64u) { let s = b64u.replace(/-/g, "+").replace(/_/g, "/"); const pad = s.length % 4; if (pad === 2) s += "=="; else if (pad === 3) s += "="; else if (pad !== 0) s += "=="; return s; } // src/watermark.ts var containerToOverlay = /* @__PURE__ */ new WeakMap(); function createAndMountHost(container, text) { const uniqueId = Math.random().toString(36).slice(2); const host = document.createElement("div"); host.setAttribute("data-rtf-wm", uniqueId); const hostStyle = [ "position:absolute !important", "inset:0 !important", "pointer-events:none !important", "z-index:2147483647 !important", "contain:layout style paint !important", "mix-blend-mode:normal !important", "all:initial !important", "position:absolute !important", "top:0 !important", "left:0 !important", "right:0 !important", "bottom:0 !important", "display:block !important" ].join("; "); host.setAttribute("style", hostStyle); const shadow = host.attachShadow({ mode: "closed" }); const escapeForSvg = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;"); const svgWidth = 320; const svgHeight = 220; const svg = `<?xml version="1.0" encoding="UTF-8"?><svg xmlns='http://www.w3.org/2000/svg' width='${svgWidth}' height='${svgHeight}' viewBox='0 0 ${svgWidth} ${svgHeight}'> <defs> <pattern id='p' width='${svgWidth}' height='${svgHeight}' patternUnits='userSpaceOnUse' patternTransform='rotate(-20)'> <text x='0' y='${Math.floor(svgHeight / 2)}' font-size='28' font-family='system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif' fill='rgba(0,0,0,0.12)' font-weight='700'>${escapeForSvg(text)}</text> </pattern> </defs> <rect width='100%' height='100%' fill='url(#p)'/></svg>`; const dataUrl = `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`; const styleEl = document.createElement("style"); styleEl.textContent = ` :host { all: initial !important; } .overlay { position: absolute !important; inset: 0 !important; pointer-events: none !important; z-index: 2147483647 !important; background-image: repeating-linear-gradient(45deg, rgba(0,0,0,0.08) 0 20px, transparent 20px 40px), ${dataUrl} !important; background-repeat: repeat, repeat !important; background-size: auto, ${svgWidth}px ${svgHeight}px !important; background-position: 0 0, 0 0 !important; user-select: none !important; } `; const overlay = document.createElement("div"); overlay.className = "overlay"; shadow.appendChild(styleEl); shadow.appendChild(overlay); if (container.isConnected && !container.contains(host)) container.appendChild(host); const observer = new MutationObserver(() => { if (container.isConnected && !container.contains(host)) container.appendChild(host); }); observer.observe(container, { childList: true }); return { host, observer }; } function mountWatermarkTo(container, text) { if (typeof document === "undefined" || !container) return; const existing = containerToOverlay.get(container); if (existing) { if (existing.text === (text || "Unlicensed")) return; unmountWatermarkFrom(container); } const { host, observer } = createAndMountHost(container, text || "Unlicensed"); containerToOverlay.set(container, { host, observer, text: text || "Unlicensed" }); } function unmountWatermarkFrom(container) { if (typeof document === "undefined" || !container) return; const entry = containerToOverlay.get(container); if (!entry) return; try { entry.observer.disconnect(); } catch { } try { entry.host.remove(); } catch { } containerToOverlay.delete(container); } // src/components/LicenseEnforcer.tsx function LicenseEnforcer({ containerRef, watermarkText = "Sineways Tablefront" }) { const license = useLicenseStatus(); useEffect3(() => { const container = containerRef.current; console.log("Mounting", license); if (!container || !license?.ready) return; if (!license.valid) { mountWatermarkTo(container, watermarkText); } else { unmountWatermarkFrom(container); } return () => { if (container) unmountWatermarkFrom(container); }; }, [license?.ready, license?.valid, containerRef, watermarkText]); useEffect3(() => { if (!license?.ready) return; if (license.valid) return; try { console.error( "[tablefront]", `License key not found or invalid. You can still use Tablefront, but a watermark will appear. To activate, add TABLEFRONT_LICENSE="your-license-key-here" to your .env file and add "tablefront activate" to your package.json build script (for example: "build": "tablefront activate && next build"). If you do not have a license key yet, you can purchase one at https://tablefront.sineways.tech/` ); } catch { } }, [license?.ready, license?.valid]); return null; } // src/defaultUIComponents.tsx import React2, { useState as useState3, useEffect as useEffect4, useRef, useContext, createContext, useCallback, useLayoutEffect, useMemo as useMemo2 } from "react"; import { createPortal } from "react-dom"; import { Fragment, jsx } from "react/jsx-runtime"; var SmartButton = React2.forwardRef(({ children, className, disabled, ...props }, ref) => { const hasTextContent = React2.useMemo(() => { const checkForText = (node) => { if (typeof node === "string" && node.trim()) return true; if (typeof node === "number") return true; if (React2.isValidElement(node)) { if (node.type === "span" || typeof node.type === "string") { return checkForText(node.props?.children); } } if (Array.isArray(node)) { return node.some(checkForText); } return false; }; return checkForText(children); }, [children]); const smartClassName = React2.useMemo(() => { if (!className) return ""; if (hasTextContent) { return className.replace(/\bsize-(\d+)\b/g, "h-$1").replace(/\bw-(\d+)\b/g, "").replace(/\s+/g, " ").trim(); } return className; }, [className, hasTextContent]); return /* @__PURE__ */ jsx( "button", { ref, className: cn( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", "hover:bg-foreground/5 hover:text-accent-foreground", '[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 [&_svg]:shrink-0', "gap-2 px-2", smartClassName ), disabled, ...props, children } ); }); SmartButton.displayName = "SmartButton"; var SimpleButton = React2.forwardRef(({ children, className, disabled, size = "default", ...props }, ref) => { const hasTextContent = React2.useMemo(() => { const checkForText = (node) => { if (typeof node === "string" && node.trim()) return true; if (typeof node === "number") return true; if (React2.isValidElement(node)) { if (node.type === "span" || typeof node.type === "string") { return checkForText(node.props?.children); } } if (Array.isArray(node)) { return node.some(checkForText); } return false; }; return checkForText(children); }, [children]); const getSizeClasses = () => { switch (size) { case "xs": return hasTextContent ? "h-8" : "h-8 w-8"; case "default": return hasTextContent ? "h-9 py-2" : "h-10 w-10"; case "sm": return hasTextContent ? "h-9 rounded-md" : "h-9 w-9 rounded-md"; case "lg": return hasTextContent ? "h-11 rounded-md" : "h-11 w-11 rounded-md"; case "icon": return hasTextContent ? "h-10" : "h-10 w-10"; case "auto": return ""; // No size classes, let className handle all sizing default: return hasTextContent ? "h-10 py-2" : "h-10 w-10"; } }; const smartClassName = React2.useMemo(() => { if (!className) return ""; if (hasTextContent) { return className.replace(/\bsize-(\d+)\b/g, "h-$1").replace(/\bw-(\d+)\b/g, "").replace(/\s+/g, " ").trim(); } return className; }, [className, hasTextContent]); return /* @__PURE__ */ jsx( "button", { ref, className: cn( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", "hover:bg-foreground/5 hover:text-accent-foreground", '[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 [&_svg]:shrink-0', "gap-2 px-2", getSizeClasses(), smartClassName ), disabled, ...props, children } ); }); SimpleButton.displayName = "SimpleButton"; var SimpleScrollArea = React2.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("relative", className), ...props, children })); SimpleScrollArea.displayName = "SimpleScrollArea"; var PopoverContext = createContext(null); var SimplePopover = React2.memo(({ children, open: controlledOpen, onOpenChange }) => { const [internalOpen, setInternalOpen] = useState3(false); const effectiveOpen = controlledOpen !== void 0 ? controlledOpen : internalOpen; const effectiveSetOpen = useCallback((b) => { onOpenChange?.(b); if (onOpenChange === void 0) setInternalOpen(b); }, [onOpenChange]); const triggerRef = useRef(null); const contentRef = useRef(null); const handleClickOutside = useCallback((e) => { if (effectiveOpen && !triggerRef.current?.contains(e.target) && !contentRef.current?.contains(e.target)) { effectiveSetOpen(false); } }, [effectiveOpen, effectiveSetOpen]); useEffect4(() => { document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [handleClickOutside]); const contextValue = useMemo2(() => ({ open: effectiveOpen, setOpen: effectiveSetOpen, triggerRef, contentRef }), [effectiveOpen, effectiveSetOpen]); return /* @__PURE__ */ jsx(PopoverContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { className: "relative inline-block", children }) }); }); SimplePopover.displayName = "SimplePopover"; var SimplePopoverTrigger = React2.memo(({ children, asChild = false }) => { const context = useContext(PopoverContext); if (!context) return /* @__PURE__ */ jsx(Fragment, { children }); const { setOpen, triggerRef, open } = context; const handleClick = useCallback((e) => { e.preventDefault(); setOpen(!open); }, [setOpen, open]); const handleRef = useCallback((el) => { triggerRef.current = el; if (React2.isValidElement(children)) { const childProps = children.props; if (typeof childProps.ref === "function") { childProps.ref(el); } else if (childProps.ref) { childProps.ref.current = el; } } }, [triggerRef, children]); if (asChild && React2.isValidElement(children)) { const childProps = children.props; const mergedClickHandler = useCallback((e) => { handleClick(e); childProps.onClick?.(e); }, [handleClick, childProps.onClick]); return React2.cloneElement( children, { onClick: mergedClickHandler, ref: handleRef } ); } return /* @__PURE__ */ jsx("div", { onClick: handleClick, ref: handleRef, children }); }); SimplePopoverTrigger.displayName = "SimplePopoverTrigger"; var SimplePopoverContent = React2.memo(({ children, className, align = "center", sideOffset = 8 }) => { const context = useContext(PopoverContext); const [position, setPosition] = useState3({ top: -9999, left: -9999 }); const [measured, setMeasured] = useState3(false); const [side, setSide] = useState3("bottom"); const innerRef = useRef(null); const updatePosition = useCallback(() => { if (!context || !context.triggerRef.current || !context.contentRef.current) return; const triggerRect = context.triggerRef.current.getBoundingClientRect(); const contentRef = context.contentRef.current; if (innerRef.current) { innerRef.current.style.maxHeight = ""; innerRef.current.style.overflowY = ""; } contentRef.style.visibility = "hidden"; const contentRect = contentRef.getBoundingClientRect(); const style2 = window.getComputedStyle(contentRef); const paddingTop = parseFloat(style2.paddingTop); const paddingBottom = parseFloat(style2.paddingBottom); const borderTop = parseFloat(style2.borderTopWidth); const borderBottom = parseFloat(style2.borderBottomWidth); const extraVertical = paddingTop + paddingBottom + borderTop + borderBottom; const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; const edgeMargin = 8; const spaceBelow = viewportHeight - triggerRect.bottom - sideOffset - edgeMargin; const spaceAbove = triggerRect.top - sideOffset - edgeMargin; let chosenSide = "bottom"; const fullHeight = contentRect.height; if (spaceBelow < fullHeight && spaceAbove >= fullHeight) { chosenSide = "top"; } else if (spaceBelow < fullHeight && spaceAbove < fullHeight) { chosenSide = spaceBelow > spaceAbove ? "bottom" : "top"; } setSide(chosenSide); let top; let maxHeight; if (chosenSide === "bottom") { top = triggerRect.bottom + sideOffset; const available = viewportHeight - top - edgeMargin; if (available < fullHeight) { maxHeight = available - extraVertical; } } else { top = triggerRect.top - fullHeight - sideOffset; const available = triggerRect.top - sideOffset - edgeMargin; if (top < edgeMargin) { top = edgeMargin; maxHeight = triggerRect.top - sideOffset - edgeMargin - extraVertical; } } let left = triggerRect.left; if (align === "center") { left = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2; } else if (align === "end") { left = triggerRect.right - contentRect.width; } if (left < edgeMargin) left = edgeMargin; if (left + contentRect.width > viewportWidth - edgeMargin) { left = viewportWidth - contentRect.width - edgeMargin; } setPosition({ top, left }); setMeasured(true); if (maxHeight !== void 0 && innerRef.current) { innerRef.current.style.maxHeight = `${Math.max(0, maxHeight)}px`; innerRef.current.style.overflowY = "auto"; } contentRef.style.visibility = ""; }, [align, sideOffset, context]); useLayoutEffect(() => { if (context?.open) { updatePosition(); } }, [context?.open, updatePosition]); const handleResize = useCallback(() => updatePosition(), [updatePosition]); const handleScroll = useCallback(() => updatePosition(), [updatePosition]); useEffect4(() => { window.addEventListener("resize", handleResize); window.addEventListener("scroll", handleScroll, true); return () => { window.removeEventListener("resize", handleResize); window.removeEventListener("scroll", handleScroll, true); }; }, [handleResize, handleScroll]); const style = useMemo2(() => ({ position: "fixed", top: `${position.top}px`, left: `${position.left}px`, visibility: measured ? "visible" : "hidden" }), [position.top, position.left, measured]); if (!context || !context.open) return null; return createPortal( /* @__PURE__ */ jsx( "div", { ref: context.contentRef, style, className: cn( "z-50 rounded-md border bg-white p-1 shadow-md", className ), children: /* @__PURE__ */ jsx("div", { ref: innerRef, className: "pr-2", children }) } ), document.body ); }); SimplePopoverContent.displayName = "SimplePopoverContent"; var SimpleTooltip = React2.memo(({ children }) => /* @__PURE__ */ jsx("div", { className: "relative group", children })); SimpleTooltip.displayName = "SimpleTooltip"; var SimpleTooltipTrigger = React2.memo(({ children, asChild }) => /* @__PURE__ */ jsx("div", { className: "inline-block", children })); SimpleTooltipTrigger.displayName = "SimpleTooltipTrigger"; var SimpleTooltipContent = React2.memo(({ children, className }) => /* @__PURE__ */ jsx("div", { className: cn( "absolute bg-card text-card-foreground animate-in fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 z-50 max-w-md whitespace-nowrap rounded-sm px-3 py-3 text-xs duration-100 shadow-[0_0_0_1px_rgba(0,0,0,0.05),0_3px_6px_rgba(0,0,0,0.1),0_5px_15px_rgba(0,0,0,0.1)] hidden group-hover:block bottom-full left-1/2 -translate-x-1/2 mb-1", className ), children })); SimpleTooltipContent.displayName = "SimpleTooltipContent"; var SimpleSwitch = React2.memo(({ checked, onCheckedChange, className, id }) => { const handleChange = useCallback((e) => { onCheckedChange?.(e.target.checked); }, [onCheckedChange]); return /* @__PURE__ */ jsx( "input", { id, type: "checkbox", checked, onChange: handleChange, className: cn("w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500", className), suppressHydrationWarning: true } ); }); SimpleSwitch.displayName = "SimpleSwitch"; var SimpleLabel = React2.memo(({ children, htmlFor, className }) => /* @__PURE__ */ jsx("label", { htmlFor, className: cn("text-sm font-medium leading-none", className), children })); SimpleLabel.displayName = "SimpleLabel"; var SimpleSeparator = React2.memo(({ className }) => /* @__PURE__ */ jsx("hr", { className: cn("my-2 border-t border-gray-200", className) })); SimpleSeparator.displayName = "SimpleSeparator"; var SimpleSettings02Icon = React2.memo(({ className }) => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", className: className || "size-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 6V4m0 2v2m0 8v2m0-2v-2m6-6h2m-2 0h-2m-6 0H4m2 0h2m8 6h2m-2 0h-2m-6 0H4m2 0h2" }) })); SimpleSettings02Icon.displayName = "SimpleSettings02Icon"; // src/icons.tsx import React3 from "react"; import { Loader2, ChevronLeftIcon, ChevronRightIcon, ChevronUp, ChevronDown, ChevronsUpDown, FilterX, Search, X, Filter, Settings, ChevronRight, ChevronDown as ChevronDownIcon } from "lucide-react"; var DefaultIcons = { // Loading and states Loader: Loader2, // Navigation and pagination PaginationPrevious: ChevronLeftIcon, PaginationNext: ChevronRightIcon, // Sorting SortAscending: ChevronUp, SortDescending: ChevronDown, SortUnsorted: ChevronsUpDown, // Expand/Collapse ExpandIcon: ChevronRight, CollapseIcon: ChevronDownIcon, // Actions ClearFilters: FilterX, Search, X, Filter, ColumnSettings: Settings }; function useDataTableIcons(iconOverrides) { return React3.useMemo(() => { if (!iconOverrides) { return DefaultIcons; } return { ...DefaultIcons, ...iconOverrides }; }, [iconOverrides]); } // src/components/SmartHeader.tsx import React4 from "react"; import { jsx as jsx2, jsxs } from "react/jsx-runtime"; function SmartHeader({ text, columnId, sortIcon, isHeaderEmpty, resizeState, headerAlignment }) { const [showText, setShowText] = React4.useState(true); const checkWidth = React4.useCallback(() => { const currentWidth = getCurrentColumnWidth(columnId); const shouldShow = shouldShowHeaderText(currentWidth); setShowText(shouldShow); }, [columnId]); React4.useEffect(() => { const timer = setTimeout(checkWidth, 0); return () => clearTimeout(timer); }, [checkWidth, resizeState.isResizing]); React4.useEffect(() => { const handleResize = () => { const timer = setTimeout(checkWidth, 100); return () => clearTimeout(timer); }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [checkWidth]); if (isHeaderEmpty || !text) { return /* @__PURE__ */ jsx2( "div", { className: "flex items-center w-full", style: { justifyContent: headerAlignment === "center" ? "center" : headerAlignment === "right" ? "flex-end" : "flex-start" }, children: sortIcon && /* @__PURE__ */ jsx2("div", { className: "flex-shrink-0", children: sortIcon }) } ); } return /* @__PURE__ */ jsxs( "div", { className: "flex items-center w-full", style: { justifyContent: headerAlignment === "center" ? "center" : headerAlignment === "right" ? "flex-end" : "flex-start" }, children: [ showText && /* @__PURE__ */ jsx2( "span", { className: "truncate min-w-0 mr-1", title: text.length > 15 ? text : void 0, children: truncateHeaderText(text) } ), sortIcon && /* @__PURE__ */ jsx2("div", { className: "flex-shrink-0", children: sortIcon }) ] } ); } // src/components/DataTableStates.tsx import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime"; function DataTableStates({ showLoadingState, showEmptyState, isLoading, loadingText, emptyStateText, tableS