{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "table", "title": "Integrated Table", "description": "Flat-props Table with columns + dataSource + rowKey, plus loading, empty, caption, function-form rowClassName, and built-in row selection (controlled / uncontrolled, with per-row gating and indeterminate select-all).", "dependencies": [ "@base-ui/react", "@hugeicons/core-free-icons", "@hugeicons/react" ], "registryDependencies": [ "table" ], "files": [ { "path": "registry/ui/table.tsx", "content": "\"use client\";\n\nimport { Checkbox as CheckboxPrimitive } from \"@base-ui/react/checkbox\";\nimport { MinusSignIcon, Tick02Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport type { ClassValue } from \"clsx\";\nimport type React from \"react\";\nimport type {\n ComponentProps,\n KeyboardEvent,\n MouseEvent,\n ReactElement,\n} from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n Table as TableRoot,\n TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// Column types — discriminated union so `render`'s `value` narrows by\n// `dataIndex`. Writing `dataIndex: \"name\"` makes the cell renderer receive\n// `T[\"name\"]` instead of `T[keyof T] | undefined`.\n// ---------------------------------------------------------------------------\n\ninterface TableColumnBase {\n /** Text alignment for both `th` and `td` of this column. */\n align?: \"left\" | \"center\" | \"right\";\n /** Applied only to body cells of this column. */\n cellClassName?: ClassValue;\n /** Applied to both the header cell and every body cell of this column. */\n className?: ClassValue;\n /** Applied only to the header cell of this column. */\n headClassName?: ClassValue;\n /** Stable identifier; also used as React key for the column. */\n key: string;\n /** Header content. */\n title: React.ReactNode;\n /** Column width — emitted as inline `style.width` on both `th` and `td`. */\n width?: number | string;\n}\n\ntype TableColumnRender = (\n value: T[K],\n record: T,\n index: number\n) => React.ReactNode;\n\n/** Column with a `dataIndex` — `render` receives the narrowed field value. */\nexport type TableColumnWithData = TableColumnBase & {\n dataIndex: K;\n} & (T[K] extends React.ReactNode\n ? { render?: TableColumnRender }\n : { render: TableColumnRender });\n\n/** Column without `dataIndex` — `render` receives `undefined` for the value. */\nexport type TableColumnWithoutData = TableColumnBase & {\n dataIndex?: undefined;\n render?: (value: undefined, record: T, index: number) => React.ReactNode;\n};\n\nexport type TableColumn =\n | TableColumnWithoutData\n | { [K in keyof T]: TableColumnWithData }[keyof T];\n\n/**\n * Strips `readonly` so the builder's inferred tuple is directly assignable to\n * the mutable `TableProps.columns` array. Without this, TS reports TS4104 in\n * the IDE when a `readonly [...]` tuple meets a mutable `T[]` parameter.\n */\ntype MutableTuple = {\n -readonly [I in keyof Cs]: Cs[I];\n};\n\n/**\n * Column builder — required for reliable `render(value, …)` narrowing on inline\n * arrays. Without it, TS contextual typing on `const cols: TableColumn[] = [...]`\n * fails under some `tsconfig` strict combos: `value` falls back to `any` and\n * `dataIndex: \"nope\"` no longer errors. Calling `defineColumns()([...])`\n * binds the generic and uses `const` inference so each element narrows by its\n * `dataIndex` literal.\n *\n * The return type drops `readonly` so it slots straight into `TableProps.columns`\n * (mutable) without TS4104 — preserves narrowing AND keeps the consumer-facing\n * type shape uniform.\n *\n * Usage:\n * const columns = defineColumns()([\n * { dataIndex: \"amount\", key: \"amount\", title: \"Amount\",\n * render: (value) => fmt(value) }, // value: number\n * ]);\n */\nexport function defineColumns() {\n return []>(\n cs: Cs\n ): MutableTuple => cs as unknown as MutableTuple;\n}\n\ntype RowKeyValue = string | number;\n\ntype RowKeyField = {\n [K in keyof T]-?: NonNullable extends RowKeyValue ? K : never;\n}[keyof T];\n\nexport type RowKey =\n | RowKeyField\n | ((record: T, index: number) => RowKeyValue);\n\n// ---------------------------------------------------------------------------\n// Internal selection checkbox — built directly on base-ui Checkbox primitive\n// because shadcn's `` wrapper hard-codes its indicator (always a\n// tick), giving indeterminate no distinct visual. Composing the primitive\n// here lets us swap the icon based on `indeterminate`. `components/ui/**` is\n// strictly read-only, so the alternative is to inline it at the call site.\n//\n// MAINTAINER NOTE: the class string below is a hand-copy of\n// `components/ui/checkbox.tsx`. When shadcn `checkbox` is upgraded\n// (`pnpm dlx shadcn@latest add checkbox --overwrite`), eyeball-diff the\n// shadcn `Checkbox` className against this one and resync.\n// ---------------------------------------------------------------------------\n\ntype SelectionCheckboxProps = ComponentProps;\n\nexport type TableCheckboxProps = Omit<\n SelectionCheckboxProps,\n | \"checked\"\n | \"children\"\n | \"defaultChecked\"\n | \"indeterminate\"\n | \"onCheckedChange\"\n>;\n\nconst ROW_INTERACTIVE_SELECTOR = [\n \"a[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n '[contenteditable=\"\"]',\n '[contenteditable=\"true\"]',\n '[role=\"button\"]',\n '[role=\"checkbox\"]',\n '[role=\"link\"]',\n '[role=\"menuitem\"]',\n].join(\",\");\n\nfunction isFromInteractiveDescendant(\n currentTarget: HTMLElement,\n target: EventTarget | null\n): boolean {\n if (!(target instanceof Element) || target === currentTarget) {\n return false;\n }\n const interactive = target.closest(ROW_INTERACTIVE_SELECTOR);\n return Boolean(interactive && currentTarget.contains(interactive));\n}\n\nfunction SelectionCheckbox({\n className,\n indeterminate,\n ...rest\n}: SelectionCheckboxProps) {\n return (\n \n {/* base-ui auto-unmounts Indicator while unchecked — exactly what we\n want. Do NOT add `keepMounted`: the tick SVG would stay in the DOM\n on unchecked rows and bleed through (visual bug fixed in round 6). */}\n svg]:size-3.5\"\n data-slot=\"easy-table-selection-indicator\"\n >\n \n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// Table\n// ---------------------------------------------------------------------------\n\nexport interface TableProps\n extends Omit, \"children\" | \"className\"> {\n /** className on ``. */\n bodyClassName?: ClassValue;\n /** Caption rendered inside ``. */\n caption?: React.ReactNode;\n /** className on ``. */\n captionClassName?: ClassValue;\n /** className on the `` root. */\n className?: ClassValue;\n /** Column definitions. */\n columns: TableColumn[];\n /**\n * Data rows. Internally treated as `[]` when `null` / `undefined` are passed,\n * so SWR / React Query's pre-response state can be passed directly.\n */\n dataSource?: T[] | null;\n /** Uncontrolled initial selected keys. */\n defaultSelectedRowKeys?: string[];\n /** className on the empty-state cell. */\n emptyClassName?: ClassValue;\n /** Empty-state message rendered when `dataSource` is empty and not loading. @default \"No data\" */\n emptyMessage?: React.ReactNode;\n /**\n * Per-row props forwarded to the selection-column Checkbox.\n *\n * Per ADR-0004: Table is a state-machine component, so per-row checkbox\n * control is in-scope coverage (Rule A), and this is the category-3 ownership\n * pattern — the returned checkbox state props are owned by the Table so\n * external props can't desync the selection state. `disabled: true` excludes\n * the row from the header \"select all\" tally.\n *\n * Keep this function pure and non-throwing — it's invoked for every row on\n * every render. Throwing here unmounts the surrounding tree (error boundary\n * is the consumer's responsibility, not the Table's).\n *\n * Recommended: pass a human-readable `aria-label` here so screen-reader users\n * hear meaningful text instead of the opaque row key. The Table's default\n * (`Select row {key}`) only fires when you don't supply one.\n */\n getCheckboxProps?: (record: T, index: number) => Partial;\n\n /** className on ``. */\n headerClassName?: ClassValue;\n\n /** When `true`, the body shows a loading message instead of rows/empty state. */\n loading?: boolean;\n /** className on the loading-state cell. */\n loadingClassName?: ClassValue;\n /** Loading message. @default \"Loading…\" */\n loadingMessage?: React.ReactNode;\n\n /**\n * Click handler for each ``. When provided, rows become focusable\n * (`tabIndex=0`, native `role=\"row\"` preserved) and respond to Enter / Space.\n * Clicks / key presses that originate inside interactive descendants or the\n * selection cell do not bubble through to row activation.\n */\n onRowClick?: (record: T, index: number) => void;\n /**\n * Called when the selection changes. `rows` mirrors the selected records in\n * `dataSource` order so consumers don't need a second lookup.\n */\n onSelectedRowKeysChange?: (keys: string[], rows: T[]) => void;\n\n /** Per-row className. Function form receives `(record, index)`. */\n rowClassName?: ClassValue | ((record: T, index: number) => ClassValue);\n /**\n * How to derive a stable string key per row. Required — falling back to\n * `index` silently breaks reordering / pagination, so we surface it.\n */\n rowKey: RowKey;\n\n /** Enable a left-side selection column with checkboxes. */\n selectable?: boolean;\n /** Controlled selected row keys. */\n selectedRowKeys?: string[];\n /** className applied to the selection column's `th` and every selection `td`. */\n selectionColumnClassName?: ClassValue;\n /**\n * Visually-hidden header text for the selection column. Used as the\n * accessible name announced before the \"Select all\" checkbox.\n * @default \"Selection\"\n */\n selectionColumnLabel?: string;\n}\n\n/**\n * Diagnostic shape returned alongside the stringified key. `kind` is set when\n * the raw `rowKey` value would produce a meaningless / corrupted string.\n */\ntype ResolvedKey = {\n key: string;\n diagnostic: \"invalid\" | \"nullish\" | \"symbol\" | null;\n};\n\nfunction stringifyRowKey(raw: unknown): ResolvedKey {\n if (raw == null) {\n return { diagnostic: \"nullish\", key: String(raw) };\n }\n if (typeof raw === \"string\" || typeof raw === \"number\") {\n return { diagnostic: null, key: String(raw) };\n }\n if (typeof raw === \"symbol\") {\n // Calling String() on a Symbol coerces it (`\"Symbol(foo)\"`) without\n // throwing — but symbol-valued keys still defeat both equality checks and\n // form submission. Surface them.\n return { diagnostic: \"symbol\", key: raw.toString() };\n }\n return { diagnostic: \"invalid\", key: String(raw) };\n}\n\nfunction resolveRowKey(\n record: T,\n index: number,\n rowKey: RowKey\n): ResolvedKey {\n if (typeof rowKey === \"function\") {\n return stringifyRowKey(rowKey(record, index));\n }\n return stringifyRowKey(record[rowKey]);\n}\n\nfunction alignClass(align: TableColumnBase[\"align\"]): string | undefined {\n if (align === \"center\") {\n return \"text-center\";\n }\n if (align === \"right\") {\n return \"text-right\";\n }\n return;\n}\n\nfunction widthStyle(\n width: TableColumnBase[\"width\"]\n): React.CSSProperties | undefined {\n if (width === undefined) {\n return;\n }\n return { width };\n}\n\n// Heuristic: a table needs an accessible name. Caption, aria-label, or\n// aria-labelledby all count. Anything truthy passes the audit.\nfunction hasAccessibleName(\n caption: React.ReactNode,\n ariaLabel: string | undefined,\n ariaLabelledBy: string | undefined\n): boolean {\n if (ariaLabel || ariaLabelledBy) {\n return true;\n }\n if (caption === undefined || caption === null || caption === false) {\n return false;\n }\n if (typeof caption === \"string\") {\n return caption.trim().length > 0;\n }\n return true;\n}\n\nfunction shouldEmitDevWarnings(): boolean {\n return (\n process.env.NODE_ENV !== \"production\" && process.env.NODE_ENV !== \"test\"\n );\n}\n\nexport function Table({\n columns,\n dataSource,\n rowKey,\n loading,\n loadingMessage = \"Loading…\",\n emptyMessage = \"No data\",\n caption,\n rowClassName,\n onRowClick,\n selectable,\n selectedRowKeys,\n defaultSelectedRowKeys,\n onSelectedRowKeysChange,\n getCheckboxProps,\n selectionColumnClassName,\n selectionColumnLabel = \"Selection\",\n className,\n headerClassName,\n bodyClassName,\n captionClassName,\n emptyClassName,\n loadingClassName,\n ...tableProps\n}: TableProps): ReactElement {\n // Runtime safety: SWR / React Query often hands `data` back as `undefined`\n // before the first response. Treat that as empty rather than throwing.\n const data: T[] = dataSource ?? [];\n\n const [internalSelected, setInternalSelected] = useState(\n defaultSelectedRowKeys ?? []\n );\n const isSelectionControlled = selectedRowKeys !== undefined;\n const currentSelected = isSelectionControlled\n ? (selectedRowKeys as string[])\n : internalSelected;\n const selectedKeySet = new Set(currentSelected);\n\n // Keep derived state as explicit linear passes. `getCheckboxProps` can change\n // identity on every parent render, so broad useMemo wrappers add complexity\n // without a reliable cache hit.\n const selectionEnabled = Boolean(selectable);\n let sawInvalidKey = false;\n let sawNullishKey = false;\n let sawSymbolKey = false;\n const rowMeta = data.map((record, index) => {\n const resolved = resolveRowKey(record, index, rowKey);\n if (resolved.diagnostic === \"invalid\") {\n sawInvalidKey = true;\n } else if (resolved.diagnostic === \"nullish\") {\n sawNullishKey = true;\n } else if (resolved.diagnostic === \"symbol\") {\n sawSymbolKey = true;\n }\n const checkboxProps =\n selectionEnabled && getCheckboxProps\n ? getCheckboxProps(record, index)\n : {};\n return {\n checkboxProps,\n index,\n key: resolved.key,\n record,\n selected: selectionEnabled && selectedKeySet.has(resolved.key),\n };\n });\n\n // Map for O(1) \"is this key still in rowMeta?\" lookups in handleToggleAll.\n // Walks rowMeta once; replaces the previous `Array.prototype.find` loop\n // which was O(N²) at scale.\n const metaByKey = new Map();\n for (const meta of rowMeta) {\n metaByKey.set(meta.key, meta);\n }\n\n const selectableRows: typeof rowMeta = [];\n let selectedSelectableCount = 0;\n for (const meta of rowMeta) {\n if (meta.checkboxProps.disabled) {\n continue;\n }\n selectableRows.push(meta);\n if (meta.selected) {\n selectedSelectableCount++;\n }\n }\n const selectableCount = selectableRows.length;\n\n const allSelected =\n selectableCount > 0 && selectedSelectableCount === selectableCount;\n const someSelected =\n selectedSelectableCount > 0 && selectedSelectableCount < selectableCount;\n\n // Fingerprint of the currently-duplicated key set — `null` when no dupes.\n // Lets us re-warn when the offending dataset changes shape, but stay quiet\n // when the same dupes scroll past.\n const dupKeyFingerprint = (() => {\n if (metaByKey.size === data.length) {\n return null;\n }\n const seen = new Set();\n const dupes = new Set();\n for (const m of rowMeta) {\n if (seen.has(m.key)) {\n dupes.add(m.key);\n } else {\n seen.add(m.key);\n }\n }\n return JSON.stringify(Array.from(dupes).sort());\n })();\n\n // Fingerprint of duplicate column keys — same pattern.\n const dupColumnKeyFingerprint = (() => {\n const seen = new Set();\n const dupes = new Set();\n for (const c of columns) {\n if (seen.has(c.key)) {\n dupes.add(c.key);\n } else {\n seen.add(c.key);\n }\n }\n if (dupes.size === 0) {\n return null;\n }\n return JSON.stringify(Array.from(dupes).sort());\n })();\n\n // ---- Dev-only warnings ----\n // Each warn lives in its own useEffect so the dependency list stays\n // honest (no over-running, no false silence) and the function complexity\n // stays low. They are only emitted in real development builds, not tests.\n const dupKeyWarnedRef = useRef>(new Set());\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (\n dupKeyFingerprint !== null &&\n !dupKeyWarnedRef.current.has(dupKeyFingerprint)\n ) {\n dupKeyWarnedRef.current.add(dupKeyFingerprint);\n console.warn(\n \"[Table] Duplicate row keys detected. Each row must produce a unique key — check your `rowKey` prop.\"\n );\n }\n }, [dupKeyFingerprint]);\n\n const dupColKeyWarnedRef = useRef>(new Set());\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (\n dupColumnKeyFingerprint !== null &&\n !dupColKeyWarnedRef.current.has(dupColumnKeyFingerprint)\n ) {\n dupColKeyWarnedRef.current.add(dupColumnKeyFingerprint);\n console.warn(\n \"[Table] Duplicate column keys detected. Each `column.key` must be unique.\"\n );\n }\n }, [dupColumnKeyFingerprint]);\n\n const nullishKeyWarnedRef = useRef(false);\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (!nullishKeyWarnedRef.current && sawNullishKey) {\n nullishKeyWarnedRef.current = true;\n console.warn(\n '[Table] `rowKey` resolved to null or undefined on at least one row, which stringifies to \"null\" / \"undefined\" and collides with other rows. Pick a `rowKey` field that is always present.'\n );\n }\n }, [sawNullishKey]);\n\n const symbolKeyWarnedRef = useRef(false);\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (!symbolKeyWarnedRef.current && sawSymbolKey) {\n symbolKeyWarnedRef.current = true;\n console.warn(\n \"[Table] `rowKey` resolved to a Symbol on at least one row. Symbols don't round-trip through equality checks or form submission — use a string / number identifier.\"\n );\n }\n }, [sawSymbolKey]);\n\n const invalidKeyWarnedRef = useRef(false);\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (!invalidKeyWarnedRef.current && sawInvalidKey) {\n invalidKeyWarnedRef.current = true;\n console.warn(\n \"[Table] `rowKey` resolved to a non-string / non-number value on at least one row. Use a stable string or number identifier.\"\n );\n }\n }, [sawInvalidKey]);\n\n const controlledDefaultWarnedRef = useRef(false);\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (\n !controlledDefaultWarnedRef.current &&\n isSelectionControlled &&\n defaultSelectedRowKeys !== undefined\n ) {\n controlledDefaultWarnedRef.current = true;\n console.warn(\n \"[Table] Both `selectedRowKeys` (controlled) and `defaultSelectedRowKeys` were passed. `defaultSelectedRowKeys` is ignored — drop one of them to silence this warning.\"\n );\n }\n }, [isSelectionControlled, defaultSelectedRowKeys]);\n\n // Lock in the controlled / uncontrolled choice from first mount; React's\n // own input warnings follow the same pattern.\n const initialControlledRef = useRef(isSelectionControlled);\n const controlledSwitchWarnedRef = useRef(false);\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (\n !controlledSwitchWarnedRef.current &&\n isSelectionControlled !== initialControlledRef.current\n ) {\n controlledSwitchWarnedRef.current = true;\n const from = initialControlledRef.current ? \"controlled\" : \"uncontrolled\";\n const to = isSelectionControlled ? \"controlled\" : \"uncontrolled\";\n console.warn(\n `[Table] \\`selectedRowKeys\\` switched from ${from} to ${to}. Pick one — toggling between the two modes resets the selection state and confuses users.`\n );\n }\n }, [isSelectionControlled]);\n\n const namelessWarnedRef = useRef(false);\n const ariaLabel = tableProps[\"aria-label\"];\n const ariaLabelledBy = tableProps[\"aria-labelledby\"];\n useEffect(() => {\n if (!shouldEmitDevWarnings()) {\n return;\n }\n if (\n !(\n namelessWarnedRef.current ||\n hasAccessibleName(caption, ariaLabel, ariaLabelledBy)\n )\n ) {\n namelessWarnedRef.current = true;\n console.warn(\n \"[Table] No accessible name. Pass `caption`, `aria-label`, or `aria-labelledby` so screen-reader users can identify this table (WCAG 1.3.1).\"\n );\n }\n }, [caption, ariaLabel, ariaLabelledBy]);\n\n const emit = (nextKeys: string[]) => {\n if (!isSelectionControlled) {\n setInternalSelected(nextKeys);\n }\n if (onSelectedRowKeysChange) {\n const rows: T[] = [];\n // Walk data so the emitted rows array stays in source order.\n const set = new Set(nextKeys);\n for (let i = 0; i < data.length; i++) {\n const { key } = resolveRowKey(data[i], i, rowKey);\n if (set.has(key)) {\n rows.push(data[i]);\n }\n }\n onSelectedRowKeysChange(nextKeys, rows);\n }\n };\n\n const handleToggleAll = (next: boolean) => {\n // Preserve any currently-selected keys that point to disabled or\n // since-removed rows — the header only governs the selectable subset.\n const preserved = currentSelected.filter((k) => {\n const meta = metaByKey.get(k);\n // Row no longer in dataSource: keep it (caller owns lifecycle).\n if (!meta) {\n return true;\n }\n return Boolean(meta.checkboxProps.disabled);\n });\n if (next) {\n const additions = selectableRows.map((r) => r.key);\n // Merge while preserving order: dataSource order first, then preserved tail.\n const seen = new Set();\n const merged: string[] = [];\n for (const k of [...additions, ...preserved]) {\n if (!seen.has(k)) {\n seen.add(k);\n merged.push(k);\n }\n }\n emit(merged);\n return;\n }\n emit(preserved);\n };\n\n const handleToggleRow = (key: string, next: boolean) => {\n if (next) {\n if (selectedKeySet.has(key)) {\n return;\n }\n emit([...currentSelected, key]);\n return;\n }\n emit(currentSelected.filter((k) => k !== key));\n };\n\n const colSpan = Math.max(1, columns.length + (selectionEnabled ? 1 : 0));\n\n const handleRowClick = (\n event: MouseEvent,\n record: T,\n index: number\n ) => {\n if (\n !onRowClick ||\n isFromInteractiveDescendant(event.currentTarget, event.target)\n ) {\n return;\n }\n onRowClick(record, index);\n };\n\n const handleRowKeyDown = (\n event: KeyboardEvent,\n record: T,\n index: number\n ) => {\n if (\n !onRowClick ||\n isFromInteractiveDescendant(event.currentTarget, event.target)\n ) {\n return;\n }\n if (event.key === \"Enter\" || event.key === \" \") {\n // Space would otherwise scroll the page.\n event.preventDefault();\n onRowClick(record, index);\n }\n };\n\n return (\n \n {caption && (\n {caption}\n )}\n \n \n {selectionEnabled && (\n \n {/* Visually-hidden column name so sighted users still get the\n \"Select all\" affordance and AT users hear a real column\n header before the checkbox. */}\n {selectionColumnLabel}\n \n \n \n {loading && (\n \n \n
\n {loadingMessage}\n
\n \n
\n )}\n {!loading && data.length === 0 && (\n \n \n
\n {emptyMessage}\n
\n \n
\n )}\n {!loading &&\n rowMeta.map(({ checkboxProps, index, key, record, selected }) => {\n const rowCls =\n typeof rowClassName === \"function\"\n ? rowClassName(record, index)\n : rowClassName;\n const interactive = Boolean(onRowClick);\n // External checkbox state props are intentionally discarded — Table\n // owns selection state. `aria-label` is left intact so consumers can\n // override the opaque default below.\n const {\n checked: _ignoredChecked,\n children: _ignoredChildren,\n defaultChecked: _ignoredDefaultChecked,\n indeterminate: _ignoredIndeterminate,\n onCheckedChange: _ignoredOnCheckedChange,\n ...passthroughCheckboxProps\n } = checkboxProps as Partial;\n return (\n handleRowClick(e, record, index)\n : undefined\n }\n onKeyDown={\n interactive\n ? (e) => handleRowKeyDown(e, record, index)\n : undefined\n }\n // Preserve the implicit row semantics of
; overriding to\n // `role=\"button\"` would strip the table structure. Keyboard\n // activation is provided via tabIndex + onKeyDown(Enter/Space).\n tabIndex={interactive ? 0 : undefined}\n >\n {selectionEnabled && (\n ) =>\n e.stopPropagation()\n }\n onKeyDown={(e: KeyboardEvent) =>\n e.stopPropagation()\n }\n >\n handleToggleRow(key, next)}\n />\n \n )}\n {columns.map((col) => {\n const value =\n col.dataIndex === undefined\n ? undefined\n : record[col.dataIndex];\n const content = col.render\n ? // The discriminated union narrows correctly externally,\n // but inside the generic body the two render signatures\n // can't be reconciled without an unsafe cast.\n // biome-ignore lint/suspicious/noExplicitAny: see comment above\n (col.render as any)(value, record, index)\n : (value as React.ReactNode);\n return (\n \n {content}\n \n );\n })}\n \n );\n })}\n \n \n );\n}\n\nexport default Table;\n", "type": "registry:component", "target": "components/easy/table.tsx" } ], "type": "registry:component" }