From d432ed681aa65228615778db603adddd4c2b8bf8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:29:01 +0000 Subject: [PATCH 1/2] feat(types): declare renderCellEditor and schema-level cellClassName on DataTableSchema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `data-table` has read both keys on its production path all along — `renderCellEditor` through a `(schema as any)` cast, `cellClassName` by destructuring it into every body cell's class — while `DataTableSchema` declared neither. `BaseSchema`'s `[key: string]: any` absorbed them, so authoring either was unchecked: a misspelling produced no error and no widget, and the cast existed for no reason other than the missing declaration. Both are now declared, and the cast is gone rather than replaced — `schema.renderCellEditor` is an ordinary typed read. The zod mirror gains both keys in the same stroke, which is the supported route for a newly declared key (`UnmirroredDeclared` is shrink-only) and keeps `zod-mirror-parity` green without touching either ledger. Nothing new runs: both keys had the same effect yesterday. What changes is that they are checked at authoring time and documented. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- ...e-render-cell-editor-and-cell-classname.md | 46 ++++++ .../docs/components/complex/data-table.mdx | 56 ++++++- .../src/renderers/complex/data-table.tsx | 21 +-- .../data-table-declared-keys-6882.test.ts | 146 ++++++++++++++++++ packages/types/src/data-display.ts | 47 ++++++ packages/types/src/zod/data-display.zod.ts | 2 + 6 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 .changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md create mode 100644 packages/types/src/__tests__/data-table-declared-keys-6882.test.ts diff --git a/.changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md b/.changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md new file mode 100644 index 0000000000..b6ed8ad55a --- /dev/null +++ b/.changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md @@ -0,0 +1,46 @@ +--- +'@object-ui/types': minor +'@object-ui/components': minor +--- + +feat(types): declare `renderCellEditor` and schema-level `cellClassName` on `DataTableSchema` + +`data-table` has read both keys on its production path all along — `renderCellEditor` +through a `(schema as any)` cast, `cellClassName` by destructuring it into every body +cell's class. Neither was declared, so authoring either one was unchecked: a +misspelling produced no error and no widget, and no editor completion offered them. +`DataTableSchema` now declares both, and the cast in `data-table.tsx` is gone rather +than replaced. + +What you can write after this change that you could not write before, exactly: +**nothing new runs.** Both keys had the same effect yesterday, because +`BaseSchema`'s `[key: string]: any` already admitted them at any type at all. What +changes is that they are now *checked* and *documented*: + +```ts +const schema: DataTableSchema = { + type: 'data-table', + columns, data, + cellClassName: 'px-2 py-1 text-sm', // every body cell — row-density padding + renderCellEditor: ({ column, value, commit, cancel }) => + column.type === 'select' + ? + : null, // null → fall through to the built-in editor +}; +``` + +⚠️ **One reject direction, deliberate.** Because the keys were previously absorbed by +the index signature as `any`, authored values of the *wrong shape* also compiled and +silently did nothing. They are now compile errors: + +- `cellClassName` is declared `string`, matching `BaseSchema.className` and + `TableColumn.cellClassName`. The renderer passes it through `cn()`, which would + also swallow `['a','b']` or `{ a: true }` — those spellings now fail to compile. + One authored spelling for a class slot is the contract. +- `renderCellEditor` is declared as the function `data-table` actually calls. A + non-function value (or a function with an incompatible context/return type) now + fails to compile instead of being ignored at runtime. + +No runtime behaviour changed anywhere, and nothing was retired. The zod mirror +(`@object-ui/types/zod`) gains both keys in the same stroke, so the validator accepts +what the published types now invite. diff --git a/content/docs/components/complex/data-table.mdx b/content/docs/components/complex/data-table.mdx index ca5031f688..4b41391a87 100644 --- a/content/docs/components/complex/data-table.mdx +++ b/content/docs/components/complex/data-table.mdx @@ -43,9 +43,22 @@ interface DataTableSchema { // Advanced features resizableColumns?: boolean; // Allow column resizing (default: true) reorderableColumns?: boolean; // Allow column reordering (default: true) - + + // Inline editing + editable?: boolean; // Enable inline cell editing (default: false) + singleClickEdit?: boolean; // Enter edit mode on single click (default: false) + renderCellEditor?: (ctx: { // Host-supplied editor widget; null -> built-in input + column: any; + row: any; + value: any; + stage: (v: any) => void; + commit: (v?: any) => void; + cancel: () => void; + }) => ReactNode; + // Styling - className?: string; // Tailwind CSS classes + className?: string; // Tailwind CSS classes on the table wrapper + cellClassName?: string; // Tailwind CSS classes on EVERY body cell // Base properties id?: string; @@ -54,6 +67,45 @@ interface DataTableSchema { } ``` +## Cell styling + +`className` styles the table wrapper; `cellClassName` is folded into **every body +cell**, which is where per-cell padding has to go — row height is a property of the +cells, not of the row element, so `rowClassName` cannot express it. A host rendering +compact rows sets it once on the schema: + +```json +{ + "type": "data-table", + "cellClassName": "px-2 py-1 text-sm", + "columns": [ + { "header": "Name", "accessorKey": "name" }, + { "header": "Amount", "accessorKey": "amount", "cellClassName": "text-right" } + ], + "data": [] +} +``` + +The per-column `cellClassName` on `TableColumn` styles one column; both apply when +both are present. + +## Inline editing + +With `editable: true` a cell enters edit mode on double-click (or on single click +with `singleClickEdit: true`) and the table renders one of its built-in editors — +text, number, date — chosen from the column's `type`. + +`renderCellEditor` lets the host supply a widget instead. The table calls it first +for every cell it is about to edit; return a node to use it, or `null` to fall +through to the built-in editor for that column. This is how `object-grid` gives a +`select` or `lookup` cell the same dedicated control the form uses, without the +component layer having to re-implement it. + +The returned node is wrapped by the table so it inherits the exit-edit +affordances the built-in editors have: Enter commits from a single-line input, +Escape cancels, and a click outside commits. Use `stage` to record a value while +staying in edit mode, `commit` to save, and `cancel` to discard. + ## Examples ### Product Inventory diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx index 3213a8c360..79a45fe3b0 100644 --- a/packages/components/src/renderers/complex/data-table.tsx +++ b/packages/components/src/renderers/complex/data-table.tsx @@ -2266,16 +2266,17 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { // re-implement select/boolean/etc. down here in the // (fields-free) component layer. Returning null means "no // widget for this type" → fall through to the built-ins. - const injectEditor = (schema as any).renderCellEditor as - | ((ctx: { - column: any; - row: any; - value: any; - stage: (v: any) => void; - commit: (v?: any) => void; - cancel: () => void; - }) => React.ReactNode) - | undefined; + // + // This used to be `(schema as any).renderCellEditor as + // (…) => React.ReactNode` — a cast that existed for one + // reason only: `DataTableSchema` did not declare the key + // this renderer has always read, so the read had to + // re-state the contract locally and the schema had to be + // opened up to let it. objectui#6882 declared it (the + // 2026-08-30 ruling), so the read is typed at its source + // and the ctx shape below is checked against the + // declaration instead of asserted against nothing. + const injectEditor = schema.renderCellEditor; if (typeof injectEditor === 'function') { const node = injectEditor({ column: col, diff --git a/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts b/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts new file mode 100644 index 0000000000..d16fc08225 --- /dev/null +++ b/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts @@ -0,0 +1,146 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#6882 — `DataTableSchema` DECLARES the two schema-level keys + * `data-table.tsx` has always read: `renderCellEditor` and `cellClassName` + * (maintainer ruling 2026-08-30, option A). + * + * ## Why this pin is compile-time, and why a runtime pin would measure nothing + * + * The enforcement being added is a TYPE declaration. `data-table` behaves + * IDENTICALLY before and after it: both keys were already read on the + * production path — `renderCellEditor` through an `(schema as any)` cast, + * `cellClassName` by destructuring into every body cell's class — so no render + * changes, no runtime value changes, and a rendering test is blind to the whole + * change. What can fail is a COMPILE. Same reading as + * `plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts` next door. + * + * ## ⚠️ The index signature is what makes the naive pin vacuous + * + * `DataTableSchema extends BaseSchema`, and `BaseSchema` carries + * `[key: string]: any`. So `DataTableSchema['renderCellEditor']` resolves to + * `any` whether or not the key is declared, and every "is this key there?" + * spelling written over the raw type answers `true` for EVERY string — + * including `bogusKeyNobodyDeclared`. A pin written that way is green before + * the fix, green after it, and measures nothing. + * + * `Declared<>` below strips the signature so NON-MEMBERSHIP can exist, which is + * the only state in which a membership question has an answer. + * + * ## ⚠️ …and `extends` alone is vacuous a second way + * + * `Expect` is satisfied by `never` (assignable + * to everything) and by `any`. `Equal<>` below is the invariant + * (function-parameter-identity) comparison instead, so neither passes. + * + * ## How the DIRECTION is proved, rather than asserted + * + * Four `@ts-expect-error` directives below are the load-bearing half. TypeScript + * reports an UNUSED `@ts-expect-error` as an error (TS2578), so each of them is + * a claim that the instrument REFUSES something: + * + * - `Expect` must be refused → the assertion helper has teeth; + * - `Equal` and `Equal` must resolve `false` → the + * comparison is invariant, not `extends`-shaped; + * - `IsDeclaredOn<'…probe…'>` must resolve `false` → the strip really removed + * the index signature, so a non-member is answerable. + * + * Break any part of the instrument — make `Expect` accept anything, make + * `Equal` bivariant, make `Declared` a no-op — and this file goes RED on the + * now-unused directive rather than quietly passing. That is the property the + * positive assertions borrow their meaning from. + */ +import { describe, it, expect } from 'vitest'; +import type { DataTableSchema } from '../data-display.js'; + +/** + * `T` with its string/number index signatures removed — the same shape + * `plugin-grid`'s `RemoveIndexSignature` uses at the seam, restated here so + * this package's pin does not depend on a downstream package. + */ +type Declared = { + [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; +}; + +/** Invariant type equality. `A extends B` is NOT this: `never` and `any` pass that. */ +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; + +/** The only assertion form used here — its constraint is what refuses `false`. */ +type Expect = T; + +/** Is `K` a DECLARED member of `DataTableSchema` (index signature stripped)? */ +type IsDeclaredOn = K extends keyof Declared ? true : false; + +/* ── Direction proofs: a broken instrument makes THIS file red ─────────────── */ + +// @ts-expect-error objectui#6882 — `Expect` must refuse `false`. Widen its constraint and this directive goes unused (TS2578). +type _ExpectRefusesFalse = Expect; + +// @ts-expect-error objectui#6882 — `never` must NOT read as equal to `true`. An `extends`-shaped comparison would let it through. +type _EqualRefusesNever = Expect>; + +// @ts-expect-error objectui#6882 — `any` must NOT read as equal to `true`, for the same reason. +type _EqualRefusesAny = Expect>; + +// @ts-expect-error objectui#6882 — a key nothing declares must answer `false`. If `Declared<>` stopped stripping `[key: string]: any`, this would answer `true` and the directive would go unused. +type _UndeclaredKeyIsRefused = Expect>; + +/* ── The assertions the card is about ─────────────────────────────────────── */ + +/** RED before objectui#6882's declaration, green after. */ +type _RenderCellEditorIsDeclared = Expect>; +/** RED before objectui#6882's declaration, green after. */ +type _CellClassNameIsDeclared = Expect>; + +/** + * The context object `data-table.tsx` actually passes to the injected editor, + * transcribed from its call site. Declaring the key with any other shape is a + * different (and false) statement about the renderer, so the shape is pinned, + * not just the membership. + */ +type CellEditorContext = { + column: any; + row: any; + value: any; + stage: (v: any) => void; + commit: (v?: any) => void; + cancel: () => void; +}; + +type _RenderCellEditorShape = Expect< + Equal< + Declared['renderCellEditor'], + ((ctx: CellEditorContext) => React.ReactNode) | undefined + > +>; + +/** Matches `TableColumn.cellClassName` and `BaseSchema.className` — both `string`. */ +type _CellClassNameShape = Expect['cellClassName'], string | undefined>>; + +describe('objectui#6882 — DataTableSchema declares the two keys data-table reads', () => { + /** + * The runtime half exists only so the compile-time pins above have a file + * vitest also runs; the assertions that matter are erased before this runs. + * It does carry one honest statement: an author writing both keys produces an + * ordinary `DataTableSchema` value, no cast anywhere. + */ + it('an author can write both keys on a plain DataTableSchema value', () => { + const authored: DataTableSchema = { + type: 'data-table', + columns: [{ header: 'Name', accessorKey: 'name' }], + data: [], + cellClassName: 'px-3 py-1', + renderCellEditor: ({ value }) => (value == null ? null : null), + }; + + expect(authored.cellClassName).toBe('px-3 py-1'); + expect(typeof authored.renderCellEditor).toBe('function'); + }); +}); diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index b76c8302f7..6b23cf0636 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -919,6 +919,36 @@ export interface DataTableSchema extends BaseSchema { * @default false */ singleClickEdit?: boolean; + /** + * Host-supplied cell editor for inline editing (objectui#6882). + * + * When a cell enters edit mode the table calls this FIRST and renders what it + * returns; returning `null` means "no widget for this column" and the table + * falls through to its built-in text / number / date inputs. It exists so a + * higher layer (e.g. `ObjectGrid`) can hand a cell the SAME dedicated widget + * the form uses for that field type — select, lookup, boolean — without the + * component layer, which is deliberately `@object-ui/fields`-free, having to + * re-implement any of them. + * + * The returned node is wrapped by the table so it gains the exit-edit + * affordances the built-in editors have (Enter commits from a single-line + * input, Escape cancels, click-outside commits); `stage` records a value + * without leaving edit mode, `commit` saves, `cancel` discards. + * + * ⚠️ Declared as of objectui#6882 (maintainer ruling 2026-08-30). `data-table` + * has read this key on the production path since inline editing landed — it + * did so through a `(schema as any)` cast, which existed for no reason other + * than this declaration's absence and is gone with it. Nothing new runs; a + * misspelling is now caught at authoring time instead of failing silently. + */ + renderCellEditor?: (ctx: { + column: any; + row: any; + value: any; + stage: (v: any) => void; + commit: (v?: any) => void; + cancel: () => void; + }) => React.ReactNode; /** * Cell value change handler * Called when a cell value is edited @@ -949,6 +979,23 @@ export interface DataTableSchema extends BaseSchema { * Function that returns CSSProperties for each row (e.g., from conditionalFormatting). */ rowStyle?: (row: any, index: number) => React.CSSProperties | undefined; + /** + * Extra CSS classes folded into EVERY body cell of the table + * (objectui#6882) — the table-level twin of {@link TableColumn.cellClassName}, + * which styles one column. Both apply when both are present. + * + * Its live use is row density: a host that renders compact / short / tall + * rows sets the per-cell padding here, because row height is a property of + * the cells, not of the `` — `rowClassName` cannot express it. + * + * ⚠️ Declared as of objectui#6882 (maintainer ruling 2026-08-30). `data-table` + * has destructured this key off the schema and folded it into every body + * cell's class all along; only the declaration was missing. `string` matches + * {@link BaseSchema.className} and {@link TableColumn.cellClassName} — the + * renderer passes it through `cn()`, which would also swallow an array or an + * object, but one authored spelling for a class slot is the contract. + */ + cellClassName?: string; /** * Number of columns to freeze (left-pin) * When set, the first N columns remain fixed while the rest scroll horizontally. diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 3da1839123..923622e420 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -244,6 +244,8 @@ export const DataTableSchema = BaseSchema.extend({ }).optional().describe('Per-record CEL predicates for the built-in row Delete item (objectui#2614)'), onSelectionChange: z.function().optional().describe('Selection change handler'), onColumnsReorder: z.function().optional().describe('Column reorder handler'), + cellClassName: z.string().optional().describe('Extra classes folded into every body cell — the table-level twin of the per-column `cellClassName`; carries row-density padding (objectui#6882)'), + renderCellEditor: z.function().optional().describe('Host-supplied inline cell editor; returning null falls through to the built-in text/number/date inputs (objectui#6882)'), frozenColumns: z.number().optional().describe('Number of frozen columns'), showRowNumbers: z.boolean().optional().describe('Show row numbers'), emptyAction: SchemaNodeSchema.optional().describe('Optional schema node rendered inside the empty-state, e.g. an "Add record" button. Lets the empty state become an actionable invitation rather than a dead end.'), From 4738f272711b3a39876303a196f5b7589b67c3c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 16:58:56 +0000 Subject: [PATCH 2/2] docs(data-table): correct the documented reach of schema-level cellClassName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The declaration shipped saying schema-level `cellClassName` applies to "every body cell". Re-measured on the render, it reaches three cells and no others: the selection-checkbox cell (`selectable`), the row-number cell (`showRowNumbers`) and the row-actions cell (`rowActions`). Data cells fold `TableColumn.cellClassName` and nothing else, so the two class slots style DISJOINT cells and never combine on one cell. The false claim shipped in four places; all four now say what is true: the `DataTableSchema` docblock, the zod `.describe()`, the "Cell styling" section of the data-table mdx, and the changeset. The mdx "compact rows" example is replaced because it demonstrated nothing: it set only the schema-level key, over `data: []`, on a table with no selection / row-number / row-actions column — so its classes reached zero cells and the table rendered its empty state. It now sets the density class on BOTH slots over real rows, which is what `ObjectGrid` does for its `rowHeight` modes. Rendered through the real renderer and measured in Chromium against real Tailwind output: row height 56px -> 28px, cell padding 16px -> 4px, font-size 16px -> 14px on every cell, data cells included. No type declaration, zod shape or renderer code changed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- ...e-render-cell-editor-and-cell-classname.md | 18 +++++++-- .../docs/components/complex/data-table.mdx | 38 ++++++++++++++----- packages/types/src/data-display.ts | 31 +++++++++++---- packages/types/src/zod/data-display.zod.ts | 2 +- 4 files changed, 66 insertions(+), 23 deletions(-) diff --git a/.changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md b/.changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md index b6ed8ad55a..f7de03743e 100644 --- a/.changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md +++ b/.changeset/6882-datatable-declare-render-cell-editor-and-cell-classname.md @@ -6,9 +6,10 @@ feat(types): declare `renderCellEditor` and schema-level `cellClassName` on `DataTableSchema` `data-table` has read both keys on its production path all along — `renderCellEditor` -through a `(schema as any)` cast, `cellClassName` by destructuring it into every body -cell's class. Neither was declared, so authoring either one was unchecked: a -misspelling produced no error and no widget, and no editor completion offered them. +through a `(schema as any)` cast, `cellClassName` by destructuring it into the class of +its three utility cells (the selection checkbox, the row number, the row actions). +Neither was declared, so authoring either one was unchecked: a misspelling produced no +error and no widget, and no editor completion offered them. `DataTableSchema` now declares both, and the cast in `data-table.tsx` is gone rather than replaced. @@ -21,7 +22,7 @@ changes is that they are now *checked* and *documented*: const schema: DataTableSchema = { type: 'data-table', columns, data, - cellClassName: 'px-2 py-1 text-sm', // every body cell — row-density padding + cellClassName: 'px-2 py-1 text-sm', // utility cells only (see below) renderCellEditor: ({ column, value, commit, cancel }) => column.type === 'select' ? @@ -41,6 +42,15 @@ silently did nothing. They are now compile errors: non-function value (or a function with an incompatible context/return type) now fails to compile instead of being ignored at runtime. +⚠️ **What the schema-level `cellClassName` actually styles.** It is NOT the +table-level twin of the per-column key: the two reach **disjoint** cells. Measured on +the render, the schema-level key is folded into the **utility** cells only — the +selection-checkbox cell, the row-number cell and the row-actions cell — while every +**data** cell folds `TableColumn.cellClassName` and nothing else. Row density is +therefore a pair of settings (`ObjectGrid` sets both), and the schema-level key alone +leaves data cells at the primitive's default `p-4`. The docblock, the zod `describe` +and `content/docs/components/complex/data-table.mdx` all say this now. + No runtime behaviour changed anywhere, and nothing was retired. The zod mirror (`@object-ui/types/zod`) gains both keys in the same stroke, so the validator accepts what the published types now invite. diff --git a/content/docs/components/complex/data-table.mdx b/content/docs/components/complex/data-table.mdx index 4b41391a87..ed00fdd0c0 100644 --- a/content/docs/components/complex/data-table.mdx +++ b/content/docs/components/complex/data-table.mdx @@ -58,7 +58,8 @@ interface DataTableSchema { // Styling className?: string; // Tailwind CSS classes on the table wrapper - cellClassName?: string; // Tailwind CSS classes on EVERY body cell + cellClassName?: string; // Tailwind CSS classes on the utility cells only + // (select / row number / row actions) // Base properties id?: string; @@ -69,25 +70,42 @@ interface DataTableSchema { ## Cell styling -`className` styles the table wrapper; `cellClassName` is folded into **every body -cell**, which is where per-cell padding has to go — row height is a property of the -cells, not of the row element, so `rowClassName` cannot express it. A host rendering -compact rows sets it once on the schema: +`className` styles the table wrapper. Body cells have **two** class slots, and they +reach **disjoint** cells — neither is a superset of the other, and no cell gets both: + +- `TableColumn.cellClassName` — the **data** cells of that one column. +- `DataTableSchema.cellClassName` — the table's **utility** cells only: the leading + selection-checkbox cell (`selectable`), the row-number cell (`showRowNumbers`), and + the trailing row-actions cell (`rowActions`). It never reaches a data cell. + +(The empty-state cell and the add-record row take neither.) + +Row density is therefore a **pair** of settings, not one. Per-cell padding is where +row height has to be expressed — height is a property of the cells, not of the row +element, so `rowClassName` cannot express it — so a compact table sets the same +density class on every column *and* on the schema, the second so the checkbox and +row-number cells stay the same height as the data beside them. That is exactly what +`object-grid` does for its `rowHeight` modes: ```json { "type": "data-table", + "selectable": true, + "showRowNumbers": true, "cellClassName": "px-2 py-1 text-sm", "columns": [ - { "header": "Name", "accessorKey": "name" }, - { "header": "Amount", "accessorKey": "amount", "cellClassName": "text-right" } + { "header": "Name", "accessorKey": "name", "cellClassName": "px-2 py-1 text-sm" }, + { "header": "Amount", "accessorKey": "amount", "cellClassName": "px-2 py-1 text-sm text-right" } ], - "data": [] + "data": [ + { "name": "Ada Lovelace", "amount": 120 }, + { "name": "Grace Hopper", "amount": 340 } + ] } ``` -The per-column `cellClassName` on `TableColumn` styles one column; both apply when -both are present. +Drop the per-column half and the data cells stay at the table primitive's default +`p-4` — the schema-level key alone does not compact a row. ## Inline editing diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 6b23cf0636..b648c58bf0 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -980,17 +980,32 @@ export interface DataTableSchema extends BaseSchema { */ rowStyle?: (row: any, index: number) => React.CSSProperties | undefined; /** - * Extra CSS classes folded into EVERY body cell of the table - * (objectui#6882) — the table-level twin of {@link TableColumn.cellClassName}, - * which styles one column. Both apply when both are present. + * Extra CSS classes folded into the table's UTILITY body cells + * (objectui#6882) — and ONLY those three, each rendered only when its + * feature is on: the leading selection-checkbox cell (`selectable`), the + * row-number cell (`showRowNumbers`), and the trailing row-actions cell + * (`rowActions`). * - * Its live use is row density: a host that renders compact / short / tall - * rows sets the per-cell padding here, because row height is a property of - * the cells, not of the `` — `rowClassName` cannot express it. + * ⚠️ It does NOT reach a data cell. A data cell folds + * {@link TableColumn.cellClassName} — the per-column key — and nothing else, + * so the two class slots style DISJOINT cells and never combine on one cell. + * (The empty-state cell and the add-record row cell take neither.) The + * population is checkable: `data-table.tsx` folds this key at exactly three + * `cn(cellClassName, …)` call sites, and the data-cell one folds + * `col.cellClassName`. + * + * Its live use is row density, and it is only half of that. Row height is a + * property of the cells, not of the `` — `rowClassName` cannot express + * it — so a host that renders compact / short / tall rows sets the per-cell + * padding on BOTH slots: `ObjectGrid` folds its density class into every + * column's `cellClassName` and passes the same class here, which is what + * keeps the checkbox and row-number cells the same height as the data beside + * them. Setting only this key leaves every data cell at the table + * primitive's default `p-4`. * * ⚠️ Declared as of objectui#6882 (maintainer ruling 2026-08-30). `data-table` - * has destructured this key off the schema and folded it into every body - * cell's class all along; only the declaration was missing. `string` matches + * has destructured this key off the schema and folded it into those three + * cells all along; only the declaration was missing. `string` matches * {@link BaseSchema.className} and {@link TableColumn.cellClassName} — the * renderer passes it through `cn()`, which would also swallow an array or an * object, but one authored spelling for a class slot is the contract. diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 923622e420..b396ab2aa4 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -244,7 +244,7 @@ export const DataTableSchema = BaseSchema.extend({ }).optional().describe('Per-record CEL predicates for the built-in row Delete item (objectui#2614)'), onSelectionChange: z.function().optional().describe('Selection change handler'), onColumnsReorder: z.function().optional().describe('Column reorder handler'), - cellClassName: z.string().optional().describe('Extra classes folded into every body cell — the table-level twin of the per-column `cellClassName`; carries row-density padding (objectui#6882)'), + cellClassName: z.string().optional().describe('Extra classes folded into the utility body cells only — the selection, row-number and row-actions cells; data cells fold the per-column `cellClassName` instead, so row density has to be set on both (objectui#6882)'), renderCellEditor: z.function().optional().describe('Host-supplied inline cell editor; returning null falls through to the built-in text/number/date inputs (objectui#6882)'), frozenColumns: z.number().optional().describe('Number of frozen columns'), showRowNumbers: z.boolean().optional().describe('Show row numbers'),