import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from "react"; import { FilterSidebarProps } from "../FilterSidebar/FilterSidebar"; import type { FilterValues } from "./filterTypes"; import type { TableVariant } from "./variants/tokens"; export type { FilterValues } from "./filterTypes"; export type { TableVariant, TableVariantTokens } from "./variants/tokens"; /** Dynamic row shape (cells, styles, callbacks). */ export type DataObject = Record; /** Row-height preset controlling row height and cell vertical padding. */ export type TableDensity = "compact" | "normal" | "comfortable"; /** Empty-state content shown when there are no rows; alternative to a raw ReactNode. */ export type TableEmptyState = { /** Rendered as the illustration above the title; omit for the default empty-state icon. */ icon?: ReactNode; /** Headline of the empty state. */ title?: string; /** Supporting line under the title. */ description?: string; }; /** Props for the main Table component: data, columns, pagination, selection, filters, and more. */ export type TableProps = { /** Every row needs a unique `id` — selection, select-all and row keys all read `row.id`. */ data: DataObject[]; /** Memoize this array: an inline one re-installs column state on every render. */ columns: TableHeaderProps[]; /** Hex override per group value; wins over the auto-assigned palette color. Only visible while grouped (Settings > Group). */ groupColors?: Record; /** Renders a "+ add item" control in each group's footer row; no-op in flat (ungrouped) mode. */ onAddItem?: (groupValue: string | number) => void; /** Seeds the initial row height only — Table Settings > Density (or saved preferences.density) takes over after the first change. */ density?: TableDensity; /** Visual preset. Defaults to `studio`; pass `"classic"` for the banded header and per-column rules. */ variant?: TableVariant; /** Content shown under a row when expanded; return null for rows with no detail and they get no chevron. Memoize it: an inline function re-renders every row. */ renderRowDetail?: (row: DataObject) => ReactNode | null; /** Controlled expanded row ids; omit for uncontrolled. */ expandedRows?: (string | number)[]; /** Fires with the full expanded id list on every chevron toggle, controlled or not. */ onExpandedRowsChange?: (ids: (string | number)[]) => void; /** Zebra rows: every second row gets the variant's `row.stripeBg` fill. Frozen and selected cells keep their own fill. */ stripe?: boolean; /** Shown when there are no rows. An object feeds the library `EmptyState`; any other node renders as-is. */ emptyState?: TableEmptyState | ReactNode; /** Shows the toolbar's global search control. Defaults true; the search only calls `onGlobalSearch`. */ isGlobalSearch?: boolean; /** @deprecated Dead — Table renders no export UI and never reads this; no known consumer usage. */ handleExportChange?: (value: string) => void | undefined; /** @deprecated Dead — same unwired export feature as handleExportChange/exportLabel. */ exportOptions?: ExportOption[]; /** @deprecated Dead — same unwired export feature as handleExportChange/exportOptions. */ exportLabel?: string; /** Skeleton rows when data is empty, otherwise dims existing rows; also true while onRefresh runs. */ isLoading?: boolean; /** @deprecated Dead — Table never destructures it; the live prop is `isTableSettings`. */ isVisiblity?: boolean; /** Adds a row-selection checkbox column plus a select-all header. Defaults false. */ isCheckbox?: boolean; /** @deprecated Dead — the header paints the variant's `header.bg`; this value is never read. */ headerBgColor?: string; /** Fill for frozen (isFreeze) and sticky action cells. Defaults to `backgroundColor.secondary`; pass a token. */ freezedBgColor?: string; /** Ink for frozen/sticky chrome cells (checkbox, action). Defaults to `text[500]`; a hardcoded hex breaks dark mode. */ freezedTextColor?: string; /** @deprecated Dead — referenced nowhere in the Table tree; no visual effect. */ tableBorderColor?: string; /** Removes the row rules and header column dividers. */ noBorders?: boolean; /** Fires with the full selected-id list on every toggle, including select-all. */ onSelection?: (selected: (string | number)[]) => void; /** Client-side paging. Defaults true; false renders every row with no pager. */ isPagination?: boolean; /** Controlled selected row ids; a new array replaces the internal selection. */ selections?: (string | number)[]; /** Fires per cell, gated on the column: a column with `node` swallows its clicks. Also makes rows tab stops; Enter or Space fires it for the first column without `node`. A node `label` arrives as its plain text. */ onRowClick?: (row: DataObject, header: Record) => void; /** Sticky-positions the trailing action/link column during horizontal scroll. Defaults true. */ isActionFreeze?: boolean; /** "server" skips client-side slicing entirely — the parent must supply totalRecords and drive paging via onPagination. */ paginationMode?: "client" | "server"; /** Replaces the pager with scroll-to-load: calls `onPagination(..., "next")` within 160px of the bottom; ignored while grouped. */ infiniteScroll?: boolean; /** Gates further loads; when omitted, derived from data.length < totalRecords. */ hasMore?: boolean; /** Parent-owned: set true while the next page is fetching — blocks repeat loads and shows the spinner. */ isLoadingMore?: boolean; /** Shows a Load more button over grouped rows instead of a pager; no effect unless the user has grouped the table. */ groupLoadMore?: boolean; /** Label and loading text of the grouped Load more button. Defaults "Load more". */ loadMoreText?: string; /** Where the grouped Load more button sits. Defaults "top". */ loadMorePosition?: "top" | "bottom"; /** Last-resort grouped-load-more chunk size, read only when `noOfRowsPerPage` is 0 or unset. */ loadMoreChunkSize?: number; /** Page size. Defaults 50; changing it later re-syncs the internal page size. */ noOfRowsPerPage?: number; /** Ignored by client paging; server/infinite/load-more use it for counts and the default hasMore. */ totalRecords?: number; /** record is the page's last row on "next"/"last", its first row on "prev"/"first" — a cursor for server paging. */ onPagination?: (page: number, noOfRecords: number, record: DataObject | undefined, direction: "next" | "prev" | "first" | "last") => void; /** Fires when the user picks a page size; the table also resets to page 1. */ onNoOfRowsPerPageChange?: (noOfRows: number) => void; /** Page-size choices; defaults are computed from data length. Server mode hides the picker unless set. */ paginationSelectOptions?: (number | string)[]; /** Explicit pixel height; overrides autoFitViewport and minVisibleRows/maxVisibleRows outright when set. */ tableMaxHeight?: number; /** Floors the auto-computed row count used for sizing only — doesn't add rows or affect pagination. */ minVisibleRows?: number; /** Row count above which height is capped and scrolls; at or under it the table grows to fit every row, even wrapped ones. Ignored once tableMaxHeight is set. */ maxVisibleRows?: number; /** Further clamps computed height to remaining viewport space below the table, recalculated on resize; ignored once tableMaxHeight is set. */ autoFitViewport?: boolean; /** Hides columns past this index on first render only — a saved preferences.columns list (or a Settings save) wins on every later render. */ defaultVisibleColumns?: number; /** Skeleton rows shown while loading with no data. Defaults 4, minimum 1. */ loadingSkeletonRows?: number; /** Toolbar bulk actions; only `isStatic` items show before any row is selected. */ headerActions?: HeaderActionsProps; /** Debounced 700ms; called with "" when the search box is closed. Filtering is the parent's job. */ onGlobalSearch?: (searchVal: string) => void; /** @deprecated Dead — Table.tsx never references it; never called, no known consumer usage. */ onColumnFilter?: (filters: FilterValues) => void; /** Gates the Settings gear (Density / Manage Columns / Group) — the real switch for column-visibility UI, unlike tableSettings or isVisiblity. */ isTableSettings?: boolean; /** @deprecated Dead — TableSettings receives it but never destructures it; no known consumer usage. */ tableSettings?: { filters?: string[]; filterSelected?: string[]; onFilterSelection?: (filters: string[]) => void; }; /** filterMode defaults to "sidebar"; Clear All in "modal" mode doesn't re-apply filters (see COMPONENT-GOTCHAS.md). */ filterSidebar?: Partial & { isFilterSidebar?: boolean; handleSidebarFilterClick?: (filters: FilterValues) => void; filterSidebarToggle?: () => void; isApplyLoading?: boolean; filterMode?: "sidebar" | "modal"; }; /** Server-persists columns/density/grouping as one JSON blob (GET on mount, POST on every Settings save). Omitting url (the default) makes it a silent no-op, not an error — the table just doesn't persist. */ preferences?: { /** Base REST endpoint for both GET and POST; empty string (the default) disables persistence entirely. */ url: string; /** Sent as a raw Authorization header value — not "Bearer "-prefixed. */ token: string; /** Distinguishes multiple saved blobs at the same url/page/name (e.g. per-view settings). */ key?: string; /** Sent alongside key/page as a query param — a server-side identifier, not rendered anywhere. */ name?: string; /** Identifies which page/screen owns this table's preferences; sent as a query param. */ page?: string; /** Scopes the preference blob per organization; sent on both GET and POST. */ orgId?: string; }; /** Shows a Refresh button when set; may return a Promise — the table shows loading until it settles. */ onRefresh?: () => void; }; /** Config for the bulk-actions toolbar (delete/update/custom items) shown for the current selection. */ export type HeaderActionsProps = { /** Built-in "Delete" action, shown only while rows are selected and `enabled` is true. */ isDelete?: { enabled?: boolean; onClick?: (selection: (string | number)[]) => void; }; /** Built-in "Mass Update" action, shown only while rows are selected and `enabled` is true. */ isUpdate?: { enabled?: boolean; onClick?: (selection: (string | number)[]) => void; }; /** Custom actions; hidden until a row is selected unless `isStatic`. `customComponent` disables button semantics. */ items?: ({ icon?: React.ReactElement; name: string; onClick?: (selection: (string | number)[]) => void; isHoverEnabled?: boolean; hoverColor?: string; customComponent?: React.ReactNode; isStatic?: boolean; } | { type: "divider"; })[]; }; /** Column definition: label, sort/search/freeze flags, and optional status or custom cell renderer. */ export type TableHeaderProps = { /** Header content. A node (icon, tooltip trigger) renders as-is but is clipped, not ellipsized, when too wide. */ label: string | number | ReactNode; /** Plain-text name for a node `label` — used by the sort/filter button names, Manage Columns, Group, filter chips and saved preferences. Ignored for a string/number label. Omitted: the node's text children, else `id`. */ labelText?: string; /** Row field this column reads (`row[id]`) and its identity in sort, filter, freeze and saved preferences. */ id: string | number; /** Enables the multi-column sort control; sorts client-side only — server mode never reorders rows. */ isSort?: boolean; /** @deprecated Never read — every column gets the filter popover regardless. */ isSearch?: boolean; /** Sticky-pins the column to the left during horizontal scroll. */ isFreeze?: boolean; /** Initial visibility only; defaultVisibleColumns and saved preferences override it. */ isHidden?: boolean; /** "status" renders the cell value as a coloured pill unless the column has a `node` renderer. */ type?: "status"; /** Pre-resolved tone per value — preferred over `statusColors` hex overrides. */ statusPalette?: Record; /** Legacy hex fill seed per value; used only when `statusPalette` lacks that value. */ statusColors?: Record; /** Field data-type; only columns that declare one appear in the Group-by picker. */ columnType?: TableColumnType; /** Horizontal alignment for both the header label and the body cells. Defaults "start", except a full-bleed status cell, which centres until this is set. */ align?: TableColumnAlign; /** Custom cell renderer; a column with `node` never fires onRowClick. */ node?: (data: DataObject) => JSX.Element | string | number | undefined; }; /** Horizontal alignment shared by a column's header label and its cells. */ export type TableColumnAlign = "start" | "center" | "end"; /** Known field data-types (open-ended via the trailing string for forward-compat). */ export type TableColumnType = "text" | "singleline" | "multiLine" | "description" | "email" | "phone" | "website" | "number" | "integer" | "decimal" | "dropdown" | "multiSelectDropdown" | "user" | "date" | "date_time" | "time" | "not_assigned" | (string & {}); /** One entry in the export-format dropdown, paired with `handleExportChange`. */ export type ExportOption = { /** Text shown for the format in the dropdown. */ label: string; /** Format value passed to `handleExportChange`. */ id: string; }; /** Current sort direction applied to one column. */ export type ColumnSortingProps = { /** `id` of the sorted column. */ column: string | number; /** "none" means the column is not sorted. */ direction: "asc" | "desc" | "none"; }; /** Internal wiring for the row-detail expander, built once by `Table`. */ export type TableRowDetail = { /** The `renderRowDetail` callback; a null result renders no detail content. */ render: (row: DataObject) => ReactNode | null; /** Expands or collapses the detail of the row with this `id`. */ toggle: (id: string | number) => void; /** Unique prefix for the DOM ids that link each toggle to its detail row. */ idPrefix: string; }; /** Props for the TableHeader row subcomponent: all columns, sorting, search, freeze, selection. */ export type TableHeaderPageProps = Pick & { handleSort: (label: string | number, type: "asc" | "desc" | "none") => void; activeHeader?: string | null; isPopoverOpen?: boolean; closePopover?: () => void; searchFeatures?: (header: string) => React.ReactNode; handleHeaderClick?: (header: string, search: boolean) => void; headerRefs: React.MutableRefObject<(HTMLTableCellElement | null)[]>; columnWidths: number[]; /** Measures the select-all cell so frozen columns can butt against its real width. */ slotRef: React.Ref; slotWidth: number; columnsSort: ColumnSortingProps[]; handleCheckbox: (selection: string | number) => void; checked: boolean | "indeterminate"; isContent: boolean; isLink: boolean; /** Adds the empty chevron column header after the checkbox. */ hasDetail?: boolean; setColumnsSearch: React.Dispatch>>; columnsSearch: Record; isSelecting?: boolean; }; /** Props for the TableBody subcomponent; renders flat rows or grouped Monday-style board rows. */ export type TableBodyPageProps = Pick & { startRow: number; /** Page size; a change swaps the rendered rows without moving startRow, so scroll resets. */ rowsPerPage: number; columnWidths: number[]; /** Rendered width of the select-all cell; frozen columns start after it. */ slotWidth: number; handleCheckbox: (selection: string | number) => void; selections: (string | number)[]; isContent: boolean; isLink: boolean; /** Row-detail expander wiring; omitted when `renderRowDetail` is not used. */ rowDetail?: TableRowDetail; /** Ids of the rows whose detail is open. */ expandedIds?: Set; scrollContainerRef?: MutableRefObject; /** Optional per-row id → accent color (Monday group stripe). */ accentColors?: Record; /** When provided, the body renders as a grouped (Monday) board instead of a flat list. */ groups?: TableRenderGroup[]; onAddItem?: (groupValue: string | number) => void; }; /** One resolved group of rows with its label/accent colors, for grouped board rendering. */ export type TableRenderGroup = { /** Group key; compared with each row's value in the group column. */ value: string | number; /** Group header text. */ label: string; /** `solid` is the left accent stripe on the header and its rows, `soft` the header fill, `text` the header ink; `onSolid` is unused. */ color: { solid: string; soft: string; text: string; onSolid?: string; }; /** Rows belonging to this group, in display order. */ rows: DataObject[]; }; /** Props for one column's search/filter popover. */ export type TableFiltersProps = { header: TableHeaderProps; setColumnsSearch: (searchVal: string) => void; columnsSearch: string; onClose: () => void; }; /** Props for a single column's header cell: sorting, filtering, freeze state, width. */ export type ColumnHeaderProps = Pick & Pick & { header: TableHeaderProps; index: number; columnWidths: number[]; slotWidth: number; /** Width of the detail chevron column to the left of frozen columns; 0 when absent. */ detailWidth?: number; headerRefs: MutableRefObject<(HTMLTableCellElement | null)[]>; columnsSearch: Record; setColumnsSearch: Dispatch>>; }; /** Pairs a column header with its current sort state, e.g. for a sort-icon renderer. */ export type SortingProps = { isSorting: ColumnSortingProps | undefined; header: TableHeaderProps; };