Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/6152-unmirrored-round4.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ minor bump, per the repository's version policy.

`displayMode` on the two chatbot types is unchanged: its refusal stays TypeScript-only, so stored
designer documents that carry `displayMode: 'floating'` parse exactly as before.

**Correction, 2026-10-01 (objectui#6152, round 5).** The "Mirrored" section above says that on a `chatbot` node `floatingConfig` stays unvalidated. That was true when this change was written, and it no longer is: objectui#6152 round 5 retired `floatingConfig` on the `chatbot` type, so writing it on a `chatbot` node is now a `tsc` error and a parse error that names `chatbot-floating`. The `chatbot-floating` node keeps its `floatingConfig`, judged member by member, as this entry says.
42 changes: 42 additions & 0 deletions .changeset/6152-unmirrored-round5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
'@object-ui/types': minor
'@object-ui/components': minor
---

feat(types)!: retire `data-table`'s `selectionStyle` and `chatbot`'s `floatingConfig` on both faces, and stop teaching `data-table`'s inline-edit flags as authored keys (objectui#6152, round 5)

**Retired (breaking).** Each key below was declared on a published TypeScript type in
`@object-ui/types` and unknown to its zod mirror in `@object-ui/types/zod`, so the tolerant
validator kept an authored value without examining it. Each is retired at once, with no alias
window:

- `data-table`: `selectionStyle` (`'always' | 'hover'`). The `data-table` renderer in
`@object-ui/components` honoured `'hover'` by hiding each row's selection checkbox until the
row was hovered, but nothing in this repository or in `objectstack` authored or produced the
key. The hover-only branch is removed: a selectable table always shows its row checkboxes,
which is what `'always'` and an unset key already did. Delete the key; `selectable` alone
turns selection on.
- `chatbot`: `floatingConfig`. The `chatbot` renderer never read it, so a value on a `chatbot`
node configured nothing. The trigger and panel it describes belong to the floating
presentation: author `type: 'chatbot-floating'` with `floatingConfig`. That node's
`floatingConfig` is unchanged on both faces, and it is still validated member by member.

For each retired key:

- the TypeScript member is now `?: never`, so writing it is a `tsc` error;
- the zod mirror refuses it by name at the key, on the tolerant validator (`AnyComponentSchema`,
`safeValidateSchema`) and on the strict authoring face (`StrictAnyComponentSchema`) alike. The
strict face used to refuse it as an unknown key with no guidance; the refusal now says what to
write instead.

Delete the key from any document or literal that carries it.

**Docs: `data-table`'s inline-edit flags are host-paired.** `editable` and `singleClickEdit`
stay declared on `DataTableSchema`, unchanged, but the `data-table` page no longer teaches them
as keys a document sets. The table only stages an edit, and saving it needs callbacks a host
supplies in code. `object-grid` sets both keys on the table it builds and supplies that save
path; a document that wants inline editing authors an `object-grid`. Their doc comments say
the same.

`@object-ui/types` and `@object-ui/components` are in the fixed release group, so this ships as a
minor bump, per the repository's version policy.
2 changes: 2 additions & 0 deletions .changeset/7654-floating-chatbot-trigger-icon-tombstone.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@ mirror must add the `retirementTombstone()` half at the same time and flip the c
rather than delete it into a vacuum.

**Correction, 2026-09-30 (objectui#6152, round 4).** The sections above say `FloatingChatbotConfig` has no zod mirror, so the `triggerIcon` refusal is type-level only and the runtime face does not change. That was true when this change was written, and it is now true on a `chatbot` node only. objectui#6152 round 4 minted the mirror on the `chatbot-floating` node, the one whose renderer reads `floatingConfig`, together with the `retirementTombstone()` half this entry asked for. So a `chatbot-floating` node that carries `floatingConfig.triggerIcon` is now refused at that path. The tripwire test was flipped for that node, not deleted.

**Correction, 2026-10-01 (objectui#6152, round 5).** The correction above says the `triggerIcon` refusal is still type-level only on a `chatbot` node. That no longer holds: objectui#6152 round 5 retired `floatingConfig` itself on the `chatbot` type, on both faces, so a `chatbot` node refuses the whole key, `triggerIcon` with it, at compile time and at parse time. The only node that still takes `floatingConfig` is `chatbot-floating`, where `triggerIcon` is refused on both faces as described above.
2 changes: 2 additions & 0 deletions .changeset/7655-chatbot-registration-authoring-faces.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,5 @@ the zod twins. The "twenty keys" above is kept as the reading of this change; th
objectui#5605 retirement entry states what the three faces declare now.

**Correction, 2026-09-30 (objectui#6152, round 4).** The "Zod twins" section above says the floating twin leaves `floatingConfig` unmirrored because no `FloatingChatbotConfig` mirror exists. That was true when this change was written, and it no longer is: objectui#6152 round 4 minted that mirror and declared `floatingConfig` on the `chatbot-floating` twin, judged member by member. The same round declared `requestBody` on `ChatbotSchema`'s twin, so all three chatbot twins now share one `requestBody` arm. `displayMode` stays unmirrored on both twins, as this entry says.

**Correction, 2026-10-01 (objectui#6152, round 5).** The section above says `ChatbotSchema` keeps `floatingConfig` as a typed member, so the `triggerIcon` tombstone reaches `chatbot` nodes. That is no longer true: objectui#6152 round 5 retired `ChatbotSchema.floatingConfig` on both faces, because the `chatbot` registration never read it. The member is a `?: never` tombstone on the TypeScript face and a named refusal on the zod twin, so a `chatbot` node refuses the whole key. `ChatbotFloatingSchema.floatingConfig` is unchanged; it never inherited the base member.
55 changes: 34 additions & 21 deletions content/docs/components/complex/data-table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,6 @@ interface DataTableSchema {
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; // the persisted record
pendingRow: any; // row + this row's staged, unsaved edits (#7188)
value: any;
stage: (v: any) => void;
commit: (v?: any) => void;
cancel: () => void;
}) => ReactNode;

// Styling
className?: string; // Tailwind CSS classes on the table wrapper
cellClassName?: string; // Tailwind CSS classes on the utility cells only
Expand Down Expand Up @@ -111,15 +98,41 @@ Drop the per-column half and the data cells stay at the table primitive's defaul

## 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`.
Inline editing is a **host-paired** capability of this table, not a flag a document
sets. The table only *stages* an edit; saving it is the job of callbacks that a host
supplies in code (`onRowSave`, `onCellChange`, `onBatchSave`), and a JSON document
cannot supply a function. So the keys that switch editing on — `editable`, and
`singleClickEdit` for entering edit mode on the first click instead of a
double-click — are set by the host on the `data-table` node it builds, together with
that save path. `object-grid` is that host: it sets both keys on its table and
passes its own save path, which writes through the data source.

**Do not author `editable` or `singleClickEdit` on a `data-table` node.** Without a
host save path the edits are staged in the table and nothing persists them. To edit
records inline from a document, author an `object-grid` instead — see
[Inline Editing](/docs/plugins/plugin-grid#inline-editing) in the grid plugin.

When a host has switched editing on, a cell enters edit mode on double-click (or on
a single click) and the table renders one of its built-in editors — text, number,
date — chosen from the column's `type`.

The host can also supply `renderCellEditor`, a function the table calls first for
every cell it is about to edit; it returns a node to use, 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. Its context is:

`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.
```plaintext
renderCellEditor?: (ctx: {
column: any;
row: any; // the persisted record
pendingRow: any; // row + this row's staged, unsaved edits (#7188)
value: any;
stage: (v: any) => void;
commit: (v?: any) => void;
cancel: () => void;
}) => ReactNode;
```

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,
Expand Down
10 changes: 6 additions & 4 deletions content/docs/plugins/plugin-chatbot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,12 @@ The six keys below are declared in the `chatbot-floating` registration's own
`inputs` (`packages/plugin-chatbot/src/renderer.tsx`). They configure the
floating action button and the panel it opens; the `chatbot` and
`chatbot-enhanced` registrations render neither and ignore them. `floatingConfig`
is declared on `ChatbotSchema` and on `ChatbotFloatingSchema` alike
(objectui#7655 declared the floating face with the same member; `ChatbotSchema`
kept its own), so authoring it on an inline node type-checks and parses - and
is dropped at render time, because the `chatbot` node never read it.
is declared on `ChatbotFloatingSchema` only. It used to be declared on
`ChatbotSchema` as well, so authoring it on a `chatbot` node type-checked and
parsed, and was then dropped at render time, because the `chatbot` node never
read it. objectui#6152 retired it there: writing `floatingConfig` on a `chatbot`
node is now a compile error **and** a parse error that names `chatbot-floating`,
the node type that reads it. `ChatbotEnhancedSchema` never declared it.

**There is no `displayMode` key.** The presentation is selected by the node's
own `type`: author a `chatbot-floating` node for the trigger-and-panel
Expand Down
22 changes: 7 additions & 15 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,6 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
reorderableColumns = true,
editable = false,
singleClickEdit = false,
selectionStyle = 'always',
rowClassName,
rowStyle,
className,
Expand Down Expand Up @@ -2405,20 +2404,13 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
}}
>
{selectable && (
<TableCell className={cn(cellClassName, "px-3", frozenColumns > 0 && "sticky left-0 z-10 bg-background", selectionStyle === 'hover' && "relative")}>
{selectionStyle === 'hover' ? (
<div className={cn("transition-opacity", isSelected ? "opacity-100" : "opacity-0 group-hover/row:opacity-100")}>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
/>
</div>
) : (
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
/>
)}
<TableCell className={cn(cellClassName, "px-3", frozenColumns > 0 && "sticky left-0 z-10 bg-background")}>
{/* Always visible: the hover-only `selectionStyle` was retired
(objectui#6152 round 5) — nothing authored or produced it. */}
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
/>
</TableCell>
)}
{showRowNumbers && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@
*
* ## `displayMode` and `floatingConfig` live on BOTH faces; `displayMode` is a tombstone on both
*
* `ChatbotSchema` keeps `floatingConfig` exactly as it had it, and
* `ChatbotFloatingSchema` declares the same member. `displayMode` was carried
* `ChatbotSchema` kept `floatingConfig` exactly as it had it, and
* `ChatbotFloatingSchema` declares the same member. (objectui#6152 round 5 has
* since RETIRED `ChatbotSchema.floatingConfig` on both faces — the `chatbot`
* registration never read it — so the member is live on the floating face only;
* the pins below read that.) `displayMode` was carried
* the same way — declared verbatim on both faces, untouched by this card —
* until objectui#7654 RETIRED it (maintainer ruling B, 2026-09-05): it is a
* `?: never` tombstone on both faces now, the designer control and the
Expand Down Expand Up @@ -163,9 +166,10 @@ export type assertionFloatingDeclaresWhatItReads = Expect<
/**
* `chatbot` keeps its WHOLE face — the six legacy keys (`loading` … `height`),
* the `onSendMessage` and (since objectui#7654) `displayMode` tombstones, and
* `floatingConfig` — exactly where they were. A `?: never` member is still a
* declared key, so the census does not move when a key is tombstoned. This
* card declared faces; it retired and moved nothing.
* `floatingConfig` (a tombstone too since objectui#6152 round 5) — exactly where
* they were. A `?: never` member is still a declared key, so the census does not
* move when a key is tombstoned. This card declared faces; it retired and moved
* nothing.
*/
export type assertionChatbotKeepsItsWholeFace = Expect<
Equal<
Expand All @@ -177,19 +181,20 @@ export type assertionChatbotKeepsItsWholeFace = Expect<
>;

/**
* The two floating keys stay TYPED on `ChatbotSchema`. Read off the member,
* not the key set: a member that fell off the declaration would not go missing
* here — it would read as `any` through `BaseSchema`'s index signature, wrong
* values would compile, and the objectui#7669 `triggerIcon` tombstone would lose
* its reach on `chatbot` nodes (all three measured on #7655's first cut, which
* moved the keys). `Equal` is what catches the `any`. `displayMode` reads as
* `undefined` since objectui#7654 tombstoned it (`?: never` without
* `exactOptionalPropertyTypes` is `never | undefined`, which collapses) — a
* reading `Equal` still tells apart from the `any` a deletion would leave.
* The two floating keys stay DECLARED on `ChatbotSchema` — as tombstones now. Read
* off the member, not the key set: a member that fell off the declaration would not
* go missing here — it would read as `any` through `BaseSchema`'s index signature,
* any value would compile on a `chatbot` node (measured on #7655's first cut, which
* moved the keys off this face by DELETION). `Equal` is what catches the `any`.
* `displayMode` reads as `undefined` since objectui#7654 tombstoned it, and
* `floatingConfig` since objectui#6152 round 5 RETIRED it on this face (the `chatbot`
* registration never read it): `?: never` without `exactOptionalPropertyTypes` is
* `never | undefined`, which collapses — a reading `Equal` still tells apart from
* the `any` a deletion would leave.
*/
export type assertionFloatingKeysStayTypedOnChatbot = [
Expect<Equal<ChatbotSchema['displayMode'], undefined>>,
Expect<Equal<ChatbotSchema['floatingConfig'], FloatingChatbotConfig | undefined>>,
Expect<Equal<ChatbotSchema['floatingConfig'], undefined>>,
];

/* ── One declaration per shared key: a pick, not a copy ──────────────────── */
Expand All @@ -216,11 +221,15 @@ export type assertionMaxToolRoundtripsIsATombstoneOnEveryFace = [
Expect<Equal<ChatbotFloatingSchema['maxToolRoundtrips'], undefined>>,
];

/* ── `displayMode` / `floatingConfig`: one type, both faces ─────────────── */
/* ── `displayMode`: one type on both faces; `floatingConfig`: live on the floating face only ── */

/**
* `floatingConfig` had one type on both faces until objectui#6152 round 5 retired the
* `chatbot` face's copy; the floating face never inherited it (its `Pick` leaves it
* out) and keeps its own declaration. The second row is the half that must NOT move.
*/
export type assertionFloatingKeysHaveOneTypeOnBothFaces = [
Expect<Equal<ChatbotFloatingSchema['displayMode'], ChatbotSchema['displayMode']>>,
Expect<Equal<ChatbotFloatingSchema['floatingConfig'], ChatbotSchema['floatingConfig']>>,
// Both faces carry the objectui#7654 tombstone, so both read `undefined`.
Expect<Equal<ChatbotFloatingSchema['displayMode'], undefined>>,
Expect<Equal<ChatbotFloatingSchema['floatingConfig'], FloatingChatbotConfig | undefined>>,
Expand Down Expand Up @@ -448,13 +457,19 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le
});

it('`floatingConfig` is mirrored here since objectui#6152 round 4, and a wrong member value is refused at its path', () => {
// This registration is the one that reads the key; `ChatbotSchema`'s twin,
// whose registration never does, still has no arm for it.
// This registration is the one that reads the key. `ChatbotSchema`'s twin,
// whose registration never does, had no arm for it until objectui#6152 round 5
// RETIRED it there — a tombstone that refuses ANY value, the valid one this
// twin accepts included. Pinned on both twins so neither half can move alone.
expect((ChatbotFloatingZod.shape as Record<string, unknown>).floatingConfig).toBeDefined();
const wrong = ChatbotFloatingZod.safeParse({ ...node, floatingConfig: { panelHeight: '520px' } });
expect(wrong.success).toBe(false);
expect(wrong.error?.issues.some((i) => i.path.join('.') === 'floatingConfig.panelHeight')).toBe(true);
expect((ChatbotZod.shape as Record<string, unknown>).floatingConfig).toBeUndefined();
const valid = { panelHeight: 520, title: 'Support' };
expect(ChatbotFloatingZod.safeParse({ ...node, floatingConfig: valid }).success).toBe(true);
const onChatbot = ChatbotZod.safeParse({ ...node, type: 'chatbot', floatingConfig: valid });
expect(onChatbot.success).toBe(false);
expect(onChatbot.error?.issues.map((i) => [i.path.join('.'), i.code])).toEqual([['floatingConfig', 'invalid_type']]);
});

it('`onClear` / `onError` / `onSend` are refused by name here too', () => {
Expand Down
Loading
Loading