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
18 changes: 18 additions & 0 deletions .changeset/7530-predicate-envelope-declared.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@object-ui/types': minor
---

**`BaseSchema.visible` / `.hidden` / `.disabled` now declare the CEL envelope object the renderer already evaluates, as one named wire type** (objectui#7530, maintainer ruling 2026-09-04, option A).

Each of the three keys goes from `boolean | string` to `boolean | ExpressionWire` on both faces, where `ExpressionWire` is `string | { dialect?: string; source: string }` — the exact string-or-envelope union `FormField.visibleWhen` and its `*When` / `*On` siblings already carried, and the exact accept set of `@object-ui/core`'s `toPredicateInput` / `hasDeclaredPredicate`. The Zod mirror's `z.union([z.boolean(), z.string()])` becomes `z.union([z.boolean(), ExpressionWireSchema])` on all three.

Two names are new on the published surface:

- `ExpressionWire` (type, main entry) — the TypeScript wire union, in `packages/types/src/expression.ts`.
- `ExpressionWireSchema` (`@object-ui/types/zod`) — its runtime twin, hoisted out of `zod/form.zod.ts` (where it was module-private) into `zod/expression.zod.ts` and imported by both `base.zod.ts` and `form.zod.ts`. One envelope type, reused by reference; no second spelling.

This is a **widening**, not a replacement, and the renderer's behaviour is untouched: `SchemaRenderer`'s `shouldHide` / `shouldDisable` chains already routed all three keys through core's one definition of "declared" and evaluated the value, and the envelope was already pinned as working on `hidden` and `disabled` — through a `Record` cast, because no key declared it. Measured before this change, `BaseSchema.safeParse({ type, hidden: { dialect: 'cel', source: 'true' } })` returned `success: false` (`invalid_union` at path `hidden`) while the identical envelope on `FormField.visibleWhen` parsed one file over; that is the gap this closes, on all three keys at once. Every boolean and string value keeps parsing and keeps type-checking unchanged. `dialect` is optional and unconstrained on the wire because the runtime reads it that way (only `'cel'` keeps its envelope on the canonical engine; anything else is unwrapped onto the legacy path).

Not changed: `hasDeclaredPredicate` (no per-key branch — option B was rejected), the `*On` / `visibleWhen` sibling keys, and `ActionSchema.condition`, which already declared the envelope inline.

Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略").
6 changes: 3 additions & 3 deletions content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ One row per declared member, in declaration order, so the list can be checked ag
| `bind` | `string` | Data-scope path this node draws its rows or value from, resolved by `useDataScope()`. Honoured only by components that call it. |
| `body` | `SchemaNode \| SchemaNode[]` | Child components rendered inside this component. |
| `children` | `SchemaNode \| SchemaNode[]` | Alias for `body`. |
| `visible` | `boolean \| string` | Visibility control. Accepts a boolean **or** a predicate expression string — the renderer evaluates this key rather than reading it as a boolean. |
| `visible` | `boolean \| string \| { dialect?: string; source: string }` | Visibility control. Accepts a boolean, a predicate expression string, **or** the CEL envelope object (`{ dialect: 'cel', source }` — what `objectstack build` emits for every authored predicate) — the renderer evaluates this key rather than reading it as a boolean. The string-or-envelope half is `ExpressionWire`, the one wire type `visibleWhen` on form fields already carries. |
| `visibleWhen` | `string` | Canonical conditional-visibility predicate (ADR-0089); the element is shown when it evaluates truthy. Evaluated **before** `visible` and `visibleOn`, and outranks both. |
| `visibleOn` | `string` | Expression for conditional visibility. **Deprecated** (ADR-0089) — use `visibleWhen`. |
| `hidden` | `boolean \| string` | Inverse of `visible` — the node is not rendered. Accepts a boolean **or** a predicate expression string, which the renderer evaluates rather than reading as a boolean; `hiddenOn` remains the sibling spelling. |
| `hidden` | `boolean \| string \| { dialect?: string; source: string }` | Inverse of `visible` — the node is not rendered. Accepts a boolean, a predicate expression string **or** the CEL envelope object (`ExpressionWire`), which the renderer evaluates rather than reading as a boolean; `hiddenOn` remains the sibling spelling. |
| `hiddenOn` | `string` | Expression for conditional hiding. |
| `disabled` | `boolean \| string` | Disabled state. Accepts a boolean **or** a predicate expression string, on the same evaluated path as `visible`. |
| `disabled` | `boolean \| string \| { dialect?: string; source: string }` | Disabled state. Accepts a boolean, a predicate expression string **or** the CEL envelope object (`ExpressionWire`), on the same evaluated path as `visible`. |
| `disabledOn` | `string` | Expression for conditional disabling. |
| `testId` | `string` | Test identifier, rendered as `data-testid`. |
| `ariaLabel` | `string \| KeyedI18nLabel` | Accessibility label, rendered as `aria-label`. `KeyedI18nLabel` is the **keyed** form (`{ key, defaultValue?, params? }`), resolved by `resolveKeyedI18nLabel` — **not** the `I18nLabel` that `label` and `description` carry. The two are structurally confusable and each returns nothing useful for the other's input. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/basic/button-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface ButtonGroupSchema {
size?: 'default' | 'sm' | 'lg' | 'icon';

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Styling
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/basic/div.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ interface DivSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/basic/span.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ interface SpanSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/complex/carousel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface CarouselSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/complex/data-table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ interface DataTableSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/complex/filter-builder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ interface FilterBuilderSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/complex/resizable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface ResizableSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/complex/scroll-area.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ interface ScrollAreaSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/data-display/statistic.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface StatisticSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/data-display/tree-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ interface TreeViewSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/disclosure/toggle-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ interface ToggleGroupSchema {
onValueChange?: (value: string | string[]) => void;

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Styling
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/feedback/toaster.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface ToasterSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/form/button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ interface ButtonSchema {
iconPosition?: 'left' | 'right'; // Icon placement

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope
loading?: boolean;

// Actions
Expand Down
4 changes: 2 additions & 2 deletions content/docs/components/form/calendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ interface CalendarSchema {
// Constraints
minDate?: Date | string; // Minimum selectable date
maxDate?: Date | string; // Maximum selectable date
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Styling
className?: string; // Tailwind CSS classes

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/form/checkbox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ interface CheckboxSchema {
label?: string;
defaultChecked?: boolean;
required?: boolean;
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope
}
```
2 changes: 1 addition & 1 deletion content/docs/components/form/combobox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface ComboboxSchema {
placeholder?: string; // Button placeholder

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Styling
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/form/date-picker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface DatePickerSchema {
onChange?: (date: Date | undefined) => void;

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Styling
className?: string;
Expand Down
4 changes: 2 additions & 2 deletions content/docs/components/form/file-upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface FileUploadSchema {
maxFiles?: number; // Maximum number of files (for multiple)

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Help text
description?: string; // Help text or description
Expand All @@ -54,7 +54,7 @@ interface FileUploadSchema {
wrapperClass?: string; // Wrapper container classes

// Base properties
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/form/input-otp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface InputOTPSchema {
onChange?: (value: string) => void;

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Styling
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/form/input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface InputSchema {
placeholder?: string;
defaultValue?: string | number;
required?: boolean;
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope
readonly?: boolean;
}
```
2 changes: 1 addition & 1 deletion content/docs/components/form/radio-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface RadioGroupSchema {
orientation?: 'horizontal' | 'vertical';

// States
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope

// Styling
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/form/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ interface SelectSchema {
options: { label: string; value: string | number }[];
placeholder?: string;
required?: boolean;
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope
}
```
2 changes: 1 addition & 1 deletion content/docs/components/form/switch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ interface SwitchSchema {
name?: string;
label?: string;
defaultChecked?: boolean;
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope
}
```
2 changes: 1 addition & 1 deletion content/docs/components/form/textarea.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ interface TextareaSchema {
placeholder?: string;
rows?: number;
required?: boolean;
disabled?: boolean | string; // boolean, or a predicate expression
disabled?: boolean | string | { dialect?: string; source: string }; // boolean, predicate expression, or CEL envelope
}
```
2 changes: 1 addition & 1 deletion content/docs/components/layout/box.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface BoxSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
}
```

Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/layout/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface PageSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/navigation/header-bar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ interface HeaderBarSchema {

// Base properties
id?: string;
visible?: boolean | string;
visible?: boolean | string | { dialect?: string; source: string };
testId?: string;
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ function renderNode(schema: Record<string, unknown>) {
*
* `renderNode` above spreads a `Record<string, unknown>` through `as never`
* because most of this file exercises shapes `BaseSchema` does not declare and
* should not: `null`, `0`, `[]`, `{}`, and the CEL envelope object. Those keep
* the cast.
* should not: `null`, `0`, `[]`, `{}`, and the EMPTY envelopes. Those keep the
* cast.
*
* The STRING form is different since objectui#7455 (ruled 2026-09-03):
* `hidden` is declared `boolean | string`, so an expression-valued `hidden` is
Expand All @@ -130,9 +130,12 @@ function renderNode(schema: Record<string, unknown>) {
* only thing that can see that; vitest cannot, because the annotation is erased
* before a single case runs.
*
* The envelope pin below deliberately stays on `renderNode`: the envelope form
* is declared on NONE of `visible` / `hidden` / `disabled`, and objectui#7530
* rules on all three together.
* The CEL ENVELOPE is declared too since objectui#7530 (ruled 2026-09-04,
* option A, on all three keys at once): `hidden` is `boolean | ExpressionWire`,
* where `ExpressionWire` is the string-or-envelope union `visibleWhen` already
* carried, so the non-empty envelope pin below runs through this helper as
* well. That its verdict is IDENTICAL to the string form's, on all three keys
* and in both polarities, is `SchemaRenderer.predicateEnvelopeDeclared.test.tsx`.
*/
function renderDeclaredNode(schema: BaseSchema) {
return render(
Expand Down Expand Up @@ -214,11 +217,11 @@ describe('SchemaRenderer `hidden` — an empty predicate is not a declared gate
expect(rendered()).toBe(true);
});

it('a non-empty CEL envelope keeps its verdict, both ways', () => {
const { unmount } = renderNode({ hidden: { dialect: 'cel', source: 'true' } });
it('a non-empty CEL envelope keeps its verdict, both ways -- through the DECLARED path, no cast (objectui#7530)', () => {
const { unmount } = renderDeclaredNode({ type: 'probe-3955', hidden: { dialect: 'cel', source: 'true' } });
expect(rendered()).toBe(false);
unmount();
renderNode({ hidden: { dialect: 'cel', source: 'false' } });
renderDeclaredNode({ type: 'probe-3955', hidden: { dialect: 'cel', source: 'false' } });
expect(rendered()).toBe(true);
});

Expand Down
Loading
Loading