From 01f6d6b99b6101ab1f4130cfcfb7941a9507753e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 17:24:53 +0000 Subject: [PATCH 1/2] feat(spec): re-introduce fieldGroups[].visibleWhen with its enforcement side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Option-A maintainer ruling (2026-08-28): the section predicate slot returns to the fieldGroups vocabulary now that the section-gating contract consumes it — CEL via ExpressionInputSchema, the ADR-0089 canonical spelling; both tombstone rows updated; deriveFieldGroupLayout passes the predicate through verbatim; strictness pins flipped from curated rejection to acceptance; ADR-0085 amended; authorable-surface regenerated by gen:schema. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- .../fieldgroups-visiblewhen-reintroduced.md | 30 +++++++++++++ content/docs/data-modeling/schema-design.mdx | 11 +++-- ...semantic-roles-over-surface-hint-blocks.md | 12 ++++++ packages/spec/authorable-surface/data.json | 1 + .../spec/src/data/field-group-layout.test.ts | 43 +++++++++++++++++++ packages/spec/src/data/field-group-layout.ts | 33 +++++++++++++- .../data/object-strictness-batch20.test.ts | 30 +++++++++++-- packages/spec/src/data/object.zod.ts | 41 ++++++++++++------ 8 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 .changeset/fieldgroups-visiblewhen-reintroduced.md diff --git a/.changeset/fieldgroups-visiblewhen-reintroduced.md b/.changeset/fieldgroups-visiblewhen-reintroduced.md new file mode 100644 index 0000000000..d5e3c9b1a6 --- /dev/null +++ b/.changeset/fieldgroups-visiblewhen-reintroduced.md @@ -0,0 +1,30 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): re-introduce `fieldGroups[].visibleWhen` — the section predicate slot, this time with its enforcement (#12715) + +Accept-set **widening** (one new optional key); additive, no existing metadata +changes meaning. + +`fieldGroups[].visibleWhen` existed briefly and was REMOVED under ADR-0085 / +ADR-0049 enforce-or-remove because no surface evaluated it. The consumer now +exists: the objectui section-gating contract renders a fieldGroups-derived +group behind a `section-divider` that carries a membership claim and gates the +whole group — header included — on its own visibility verdict. The maintainer +ruled (2026-08-28, #12715) the slot re-declared together with that enforcement, +closing the enforce-or-remove loop in both directions. + +- **`visibleWhen`** on `ObjectFieldGroupSchema` — CEL via + `ExpressionInputSchema`, the ADR-0089 canonical spelling shared with field, + action and row predicates (bare string shorthand normalizes to the + `{ dialect: 'cel', source }` envelope). FALSE, or a faulting predicate + (fail-closed), hides the whole group; TRUE or absent shows it. +- **`deriveFieldGroupLayout` passes the predicate through** to the derived + `FieldGroupSection` verbatim (string or envelope, tolerant of un-parsed + metadata like its collapse-alias handling); evaluation stays the renderer's. +- The `visibleOn` spelling stays rejected, its guidance now pointing at the + real slot instead of a removal notice; the `visibleWhen` tombstone row is + retired (the key is real again). + + diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx index 444a5c4187..09f9c3073f 100644 --- a/content/docs/data-modeling/schema-design.mdx +++ b/content/docs/data-modeling/schema-design.mdx @@ -314,17 +314,20 @@ export const Account = ObjectSchema.create({ | `icon` | `string?` | Lucide/Material icon name for the group header | | `description` | `string?` | Optional description under the header | | `collapse` | `'none' \| 'expanded' \| 'collapsed'` (default `'none'`) | Collapse behaviour on every surface: `'none'` = always open (no toggle), `'expanded'` = collapsible and starts open, `'collapsed'` = collapsible and starts closed (ADR-0085; replaces the deprecated `defaultExpanded` flag, which is still accepted as an alias) | +| `visibleWhen` | `Expression?` (CEL) | Section visibility predicate — the whole group, header included, is shown only when it evaluates TRUE, else hidden (fail-closed). Same spelling and CEL vocabulary as field and action predicates (ADR-0089); write a bare string (`visibleWhen: "record.type == 'invoice'"`) or the full expression envelope (ADR-0085 §5) | ### Supported Migrations (MVP) ✅ Add / rename / delete / reorder groups — edit the `fieldGroups` array. ✅ Assign an existing field to a group — set `Field.group`. +✅ Show or hide a whole group conditionally — set `visibleWhen` on the group +(ADR-0085 §5). + ⏳ Deferred (future iterations): explicit per-field in-group ordering, nested -groups, group-level visibility predicates. Groups render identically on forms, -modals, and detail pages; when a single page needs a bespoke layout beyond -groups, assign it a custom Page schema instead of adding per-surface hints -here (ADR-0085). +groups. Groups render identically on forms, modals, and detail pages; when a +single page needs a bespoke layout beyond groups, assign it a custom Page +schema instead of adding per-surface hints here (ADR-0085). --- diff --git a/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md b/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md index 2da5b82711..dcb08d4690 100644 --- a/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md +++ b/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md @@ -115,3 +115,15 @@ Execution landed across both repos; differences from the proposal are noted inli - **Cross-surface consumption** — kanban default card fields ride `highlightFields` (objectui#2541), alongside grids/lists/detail strip. - **Verification** — parse: `@objectstack/spec` suite; served pipeline: `packages/qa/dogfood/test/semantic-roles.dogfood.test.ts`; real-backend browser pass over the four detail shapes (grouped / ungrouped / `stageField: false` / related-heavy): framework#3019, runbook + results in `docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md`, and a permanent Playwright spec in `examples/app-showcase/e2e/detail-shapes.spec.ts`. - **Consequence surfaced by the browser pass** — because detail bodies hide strip fields, a fully-highlighted group silently disappears from detail pages. Judged working-as-intended (one curated list, every surface) but author-surprising — hence the `field-group-shadowed` warning and the semantic-zoo fixture keeping one non-highlighted member per group. + +## Addendum (2026-08-28): `fieldGroups[].visibleWhen` re-introduced, with its enforcement + +§3 removed the group-level visibility slot under the ADR-0049 enforce-or-remove rule, with the exit condition stated in the same breath: *"re-add with its enforcement when a consumer ships."* That consumer shipped — objectui#6236 landed the section-gating contract (a `section-divider` row carries a membership claim, `FormField.fields`, and the form renderer gates the whole claimed group — header included — on the divider's own visibility verdict). Its six-way ablation matrix then measured the two fieldGroups-derived synthesis paths (ModalForm / DrawerForm `derivedSections`) as structurally fail-open for exactly one reason: this vocabulary had no predicate slot for the divider to carry (#12715). + +Maintainer ruling (2026-08-28, #12715): **option A — re-introduce the slot, this time with enforcement**, closing the loop as designed: + +- **`fieldGroups[].visibleWhen`** — CEL via `ExpressionInputSchema`, the ADR-0089 canonical spelling shared with field, action and row predicates. No second predicate language, no new semantics. +- **§5's shared derivation** (`deriveFieldGroupLayout`) passes the predicate through to the derived section verbatim (bare string or expression envelope), alongside `icon`/`description`/`collapse`. Evaluation stays the renderer's: grouping is static layout, visibility is per-record state. +- **Whole-group disappearance (header included) rides the landed objectui#6236 contract unchanged** — FALSE (or a faulting predicate, fail-closed) hides the divider and every claimed member. +- The `visibleOn` spelling stays rejected with guidance pointing at `visibleWhen` (the #8382 posture: pointed, not aliased). +- Both directions of the ADR-0049 trichotomy are now on this key's record: removed while unenforced, re-declared when declared = enforced holds on day one. diff --git a/packages/spec/authorable-surface/data.json b/packages/spec/authorable-surface/data.json index 44bb0ecc1b..4716b2025d 100644 --- a/packages/spec/authorable-surface/data.json +++ b/packages/spec/authorable-surface/data.json @@ -690,6 +690,7 @@ "data/ObjectFieldGroup:icon", "data/ObjectFieldGroup:key", "data/ObjectFieldGroup:label", + "data/ObjectFieldGroup:visibleWhen", "data/PerOperationRequiredPermissions:create", "data/PerOperationRequiredPermissions:delete", "data/PerOperationRequiredPermissions:read", diff --git a/packages/spec/src/data/field-group-layout.test.ts b/packages/spec/src/data/field-group-layout.test.ts index 07fad8cabc..8d2bab8b91 100644 --- a/packages/spec/src/data/field-group-layout.test.ts +++ b/packages/spec/src/data/field-group-layout.test.ts @@ -114,6 +114,49 @@ describe('deriveFieldGroupLayout (ADR-0085 §5)', () => { })!; expect(sections[0].label).toBe('billing'); }); + + // `visibleWhen` passthrough — the ADR-0049 re-introduction WITH enforcement: + // the slot only exists because the renderer's section-gating contract now + // evaluates it, and the derivation's whole job here is to CARRY it, verbatim, + // in both the shapes real metadata arrives in. + describe('visibleWhen passthrough (ADR-0085 §5 re-introduction)', () => { + const derive = (group: Record) => + deriveFieldGroupLayout({ + fieldGroups: [group], + fields: { a: { group: 'g' } }, + })![0]; + + it('carries a bare CEL string through verbatim (author / bare-DB form)', () => { + expect(derive({ key: 'g', label: 'G', visibleWhen: "record.type == 'invoice'" }).visibleWhen) + .toBe("record.type == 'invoice'"); + }); + + it('carries an Expression envelope through verbatim (post-parse form)', () => { + const envelope = { dialect: 'cel', source: "record.type == 'invoice'" }; + expect(derive({ key: 'g', label: 'G', visibleWhen: envelope }).visibleWhen).toBe(envelope); + }); + + it('omits the key entirely when the group declares no predicate', () => { + expect('visibleWhen' in derive({ key: 'g', label: 'G' })).toBe(false); + }); + + it('drops non-predicate shapes instead of forwarding them (fail-closed renderer would hide the group)', () => { + expect('visibleWhen' in derive({ key: 'g', label: 'G', visibleWhen: 42 })).toBe(false); + expect('visibleWhen' in derive({ key: 'g', label: 'G', visibleWhen: '' })).toBe(false); + expect('visibleWhen' in derive({ key: 'g', label: 'G', visibleWhen: ['record.x'] })).toBe(false); + expect('visibleWhen' in derive({ key: 'g', label: 'G', visibleWhen: null })).toBe(false); + }); + + it('never stamps a predicate on the trailing ungrouped bucket', () => { + const sections = deriveFieldGroupLayout({ + fieldGroups: [{ key: 'g', label: 'G', visibleWhen: 'record.a > 0' }], + fields: { a: { group: 'g' }, loose: {} }, + })!; + const trailing = sections[sections.length - 1]; + expect(trailing.key).toBeUndefined(); + expect('visibleWhen' in trailing).toBe(false); + }); + }); }); /** diff --git a/packages/spec/src/data/field-group-layout.ts b/packages/spec/src/data/field-group-layout.ts index dff0c823ed..85ded1c036 100644 --- a/packages/spec/src/data/field-group-layout.ts +++ b/packages/spec/src/data/field-group-layout.ts @@ -22,7 +22,12 @@ * - hidden fields never surface; * - `collapse` passes through (deprecated `defaultExpanded` / * `collapsible`+`collapsed` aliases are honoured for pre-ADR-0085 - * metadata that reaches consumers un-normalized, e.g. bare DB rows). + * metadata that reaches consumers un-normalized, e.g. bare DB rows); + * - `visibleWhen` passes through verbatim (bare CEL string or Expression + * envelope — the ADR-0049 re-introduction with enforcement): EVALUATION + * is the renderer's section-gating contract (FALSE hides the whole + * group, header included, fail-closed), not this helper's — grouping is + * static layout, visibility is per-record state. * * Returns `null` when grouping does not apply — no declared groups, or no * visible field references one — so callers fall back to their existing @@ -32,6 +37,16 @@ /** Collapse behaviour of a derived section (mirrors ObjectFieldGroupSchema.collapse). */ export type FieldGroupCollapse = 'none' | 'expanded' | 'collapsed'; +/** + * Carried section predicate (mirrors `ObjectFieldGroupSchema.visibleWhen`): + * a bare CEL string (author/bare-DB form) or an Expression envelope + * (`{ dialect, source, … }`, the post-parse form). Passed through verbatim — + * the renderer's section-gating contract evaluates it; this module only + * carries it. Structural on purpose: the helper stays dependency-free and + * tolerant of un-parsed metadata, same as its `collapse`-alias handling. + */ +export type FieldGroupVisibleWhen = string | { readonly [key: string]: unknown }; + /** One derived section. `key` is absent on the trailing ungrouped bucket. */ export interface FieldGroupSection { /** Group machine key; i18n anchor (`…objects.{obj}._sections.{key}.label`). Absent = ungrouped bucket. */ @@ -44,6 +59,8 @@ export interface FieldGroupSection { description?: string; /** Collapse behaviour; 'none' when the group declared nothing. */ collapse: FieldGroupCollapse; + /** Section visibility predicate, passed through verbatim; absent = always visible. Never present on the ungrouped bucket. */ + visibleWhen?: FieldGroupVisibleWhen; /** Member field NAMES in field-declaration order. Renderers resolve defs themselves. */ fields: string[]; } @@ -86,10 +103,20 @@ export const FIELD_GROUP_SYSTEM_FIELDS: ReadonlySet = new Set([ type AnyRec = Record; /** Normalize one declared group entry; null for malformed/keyless entries. */ -function readGroup(g: unknown): { key: string; label?: string; icon?: string; description?: string; collapse: FieldGroupCollapse } | null { +function readGroup(g: unknown): { key: string; label?: string; icon?: string; description?: string; collapse: FieldGroupCollapse; visibleWhen?: FieldGroupVisibleWhen } | null { if (!g || typeof g !== 'object' || Array.isArray(g)) return null; const grp = g as AnyRec; if (typeof grp.key !== 'string' || grp.key.length === 0) return null; + // Tolerant passthrough, same posture as the collapse aliases below: a bare + // CEL string or an Expression envelope rides through; any other shape is + // dropped rather than handed to a renderer as a malformed predicate (which + // would fail-closed and hide the group for a value that was never a + // predicate at all). + const vw = grp.visibleWhen; + const visibleWhen: FieldGroupVisibleWhen | undefined = + (typeof vw === 'string' && vw.length > 0) || (typeof vw === 'object' && vw !== null && !Array.isArray(vw)) + ? (vw as FieldGroupVisibleWhen) + : undefined; let collapse: FieldGroupCollapse = 'none'; if (grp.collapse === 'expanded' || grp.collapse === 'collapsed' || grp.collapse === 'none') { collapse = grp.collapse; @@ -106,6 +133,7 @@ function readGroup(g: unknown): { key: string; label?: string; icon?: string; de icon: typeof grp.icon === 'string' ? grp.icon : undefined, description: typeof grp.description === 'string' ? grp.description : undefined, collapse, + ...(visibleWhen !== undefined ? { visibleWhen } : {}), }; } @@ -155,6 +183,7 @@ export function deriveFieldGroupLayout(def: unknown): FieldGroupSection[] | null ...(g.icon !== undefined ? { icon: g.icon } : {}), ...(g.description !== undefined ? { description: g.description } : {}), collapse: g.collapse, + ...(g.visibleWhen !== undefined ? { visibleWhen: g.visibleWhen } : {}), fields: names, }); } diff --git a/packages/spec/src/data/object-strictness-batch20.test.ts b/packages/spec/src/data/object-strictness-batch20.test.ts index 35d1c963cc..822e4be246 100644 --- a/packages/spec/src/data/object-strictness-batch20.test.ts +++ b/packages/spec/src/data/object-strictness-batch20.test.ts @@ -474,9 +474,33 @@ describe('#4001 批 20 — curation is anchored to the sibling contract that mak }); }); - describe('tombstones — a retired key\'s rejection carries its upgrade', () => { - it('`fieldGroups[].visibleWhen` says it was REMOVED, not misspelled', () => { - const msg = rejectOnObject({ fieldGroups: [{ key: 'g', label: 'G', visibleWhen: 'true' }] }); + describe('the re-opened predicate slot — `fieldGroups[].visibleWhen` is a real key again (ADR-0049 loop closed)', () => { + // This block REPLACED a tombstone pin: `visibleWhen` was removed under + // ADR-0085 / ADR-0049 enforce-or-remove while nothing evaluated it, and + // re-declared once the form renderer's section-gating contract shipped. + // The pins flip accordingly — acceptance (both authorable shapes, with the + // ExpressionInputSchema normalization) instead of a curated rejection. + it('accepts the bare-string CEL shorthand and normalizes it to the envelope', () => { + const obj = accept(ObjectSchema, { + ...OBJ, + fields: { name: { type: 'text', label: 'Name', group: 'g' } }, + fieldGroups: [{ key: 'g', label: 'G', visibleWhen: "record.type == 'invoice'" }], + }) as { fieldGroups?: Array<{ visibleWhen?: unknown }> }; + expect(obj.fieldGroups?.[0]?.visibleWhen).toEqual({ dialect: 'cel', source: "record.type == 'invoice'" }); + }); + + it('accepts the full Expression envelope, standalone and through the carrier', () => { + accept(ObjectFieldGroupSchema, { key: 'g', label: 'G', visibleWhen: { dialect: 'cel', source: 'record.amount > 0' } }); + accept(ObjectSchema, { + ...OBJ, + fields: { name: { type: 'text', label: 'Name', group: 'g' } }, + fieldGroups: [{ key: 'g', label: 'G', visibleWhen: { dialect: 'cel', source: 'record.amount > 0' } }], + }); + }); + + it('`visibleOn` stays rejected, and its guidance now names the real slot', () => { + const msg = rejectOnObject({ fieldGroups: [{ key: 'g', label: 'G', visibleOn: 'true' }] }); + expect(msg).toContain('visibleWhen'); expect(msg).toContain('ADR-0085'); }); }); diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 3b59e0aee8..b24e8834f1 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -1100,13 +1100,16 @@ export const LifecycleSchema = lazySchema(() => strictObject({ * Deferred (not part of MVP): * - explicit per-field in-group ordering * - nested groups / sub-groups - * - group-level visibility predicates (a `visibleOn` key existed here - * briefly with no consumer anywhere; removed per ADR-0085 / ADR-0049 - * enforce-or-remove — re-add together with its enforcement when a - * surface actually evaluates it) + * + * Group-level visibility (`visibleWhen`) closed the ADR-0049 enforce-or-remove + * loop in BOTH directions: a predicate slot existed here briefly with no + * consumer on any surface and was REMOVED per ADR-0085 / ADR-0049; it is + * re-declared now that the form renderer's section-gating contract actually + * evaluates a section predicate (the whole group — header included — hides + * when it is FALSE), so declared = enforced held on day one both times. * * Derivation semantics (declared order, empty groups dropped, ungrouped - * trailing bucket, collapse passthrough) are single-sourced in + * trailing bucket, collapse + visibleWhen passthrough) are single-sourced in * `deriveFieldGroupLayout` (field-group-layout.ts, ADR-0085 §5) — UI * renderers consume that helper instead of re-implementing the rules. * @@ -1148,15 +1151,10 @@ export const ObjectFieldGroupSchema = lazySchema(() => strictObject({ '`expanded` is not a group key — use the `collapse` enum: ' + "`collapse: 'expanded'` (collapsible, starts open), `'collapsed'` (starts closed), " + "or `'none'` (always open, no toggle).", - // Tombstone: this key really did exist here, briefly, reading nothing. - visibleWhen: - '`visibleWhen` was REMOVED from field groups (ADR-0085 / ADR-0049 enforce-or-remove) ' + - '— it existed here briefly with no consumer on any surface, so it never gated ' + - 'anything. Gate the individual fields, or assign a Page for per-surface control.', visibleOn: - '`visibleOn` is not a field-group key — group-level visibility predicates were ' + - 'removed under ADR-0085 / ADR-0049 (nothing evaluated them). Gate the individual ' + - 'fields, or assign a Page.', + '`visibleOn` is not a field-group key — the section predicate slot is `visibleWhen` ' + + '(ADR-0089 canonical spelling; re-added with its enforcement under ADR-0085 §5). ' + + 'Write `visibleWhen: ` — FALSE hides the whole group, header included.', }, }, { /** Group key — referenced by `Field.group` to assign a field to this group. Must be snake_case. */ @@ -1173,6 +1171,23 @@ export const ObjectFieldGroupSchema = lazySchema(() => strictObject({ /** Optional description / help text shown under the group header. */ description: z.string().optional().describe('Optional description shown under the group header'), + /** + * [ADR-0085 §5] Section visibility predicate — CEL, the ADR-0089 canonical + * spelling shared with field/action predicates. Evaluated by the form + * renderer's section-gating contract against the record in scope: FALSE (or + * a faulting predicate — fail-closed) hides the WHOLE group, header + * included; TRUE (or absent) shows it. `deriveFieldGroupLayout` passes it + * through to the derived section verbatim. + * + * History (ADR-0049 enforce-or-remove, both directions): the slot was + * removed while nothing on any surface evaluated it, and re-declared + * together with its enforcement once the section-gating consumer shipped — + * declared = enforced on day one, both times. + */ + visibleWhen: ExpressionInputSchema.optional().describe( + "Section visibility predicate (CEL) — the whole group (header included) is shown only when TRUE, else hidden (fail-closed). e.g. P`record.type == 'invoice'`", + ), + /** * [ADR-0085] Collapse behaviour of the group's rendered section, on every * surface (form, detail, drawer). One enum, three valid states — replaces From d19f166eef9d5d72fe5e23f66c50b48464b55714 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 18:06:55 +0000 Subject: [PATCH 2/2] chore(spec): regenerate docs reference, api-surface and export-origins gen:docs picks up the new ObjectFieldGroup.visibleWhen row; api-surface and export-origins pick up the FieldGroupVisibleWhen type export. Repo tooling only (gen:docs / gen:api-surface / gen:export-origins), no hand edits. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- content/docs/references/data/object.mdx | 2 ++ packages/spec/api-surface/data.json | 1 + packages/spec/export-origins/data.json | 1 + 3 files changed, 4 insertions(+) diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 8ea613193b..284921c61a 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -307,6 +307,7 @@ const result = ApiMethod.parse(data); | **label** | `string` | ✅ | Group display label | | **icon** | `string` | optional | Icon name (Lucide/Material) for the group header | | **description** | `string` | optional | Optional description shown under the group header | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Section visibility predicate (CEL) — the whole group (header included) is shown only when TRUE, else hidden (fail-closed). e.g. P`record.type == 'invoice'` | | **collapse** | `Enum<'none' \| 'expanded' \| 'collapsed'>` | optional (default: `"none"`) | [ADR-0085] Section collapse behaviour: 'none' (always open, no toggle), 'expanded' (collapsible, starts open), 'collapsed' (collapsible, starts closed). | | **defaultExpanded** | `boolean` | optional | [DEPRECATED → collapse] true → 'expanded', false → 'collapsed'. | | **collapsible** | `boolean` | optional | [DEPRECATED → collapse] Boolean pair with `collapsed`; use the `collapse` enum. | @@ -656,6 +657,7 @@ External datasource binding (ADR-0015) | **label** | `string` | ✅ | Group display label | | **icon** | `string` | optional | Icon name (Lucide/Material) for the group header | | **description** | `string` | optional | Optional description shown under the group header | +| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Section visibility predicate (CEL) — the whole group (header included) is shown only when TRUE, else hidden (fail-closed). e.g. P`record.type == 'invoice'` | | **collapse** | `Enum<'none' \| 'expanded' \| 'collapsed'>` | optional (default: `"none"`) | [ADR-0085] Section collapse behaviour: 'none' (always open, no toggle), 'expanded' (collapsible, starts open), 'collapsed' (collapsible, starts closed). | | **defaultExpanded** | `boolean` | optional | [DEPRECATED → collapse] true → 'expanded', false → 'collapsed'. | | **collapsible** | `boolean` | optional | [DEPRECATED → collapse] Boolean pair with `collapsed`; use the `collapse` enum. | diff --git a/packages/spec/api-surface/data.json b/packages/spec/api-surface/data.json index ad09997433..4e577207a4 100644 --- a/packages/spec/api-surface/data.json +++ b/packages/spec/api-surface/data.json @@ -263,6 +263,7 @@ "Field (type)", "FieldGroupCollapse (type)", "FieldGroupSection (interface)", + "FieldGroupVisibleWhen (type)", "FieldInput (type)", "FieldMaskingKeep (type)", "FieldMaskingKeepSchema (const)", diff --git a/packages/spec/export-origins/data.json b/packages/spec/export-origins/data.json index ff470e75d0..60e669e3e9 100644 --- a/packages/spec/export-origins/data.json +++ b/packages/spec/export-origins/data.json @@ -263,6 +263,7 @@ "Field": "src/data/field.zod.ts#Field (type)", "FieldGroupCollapse": "src/data/field-group-layout.ts#FieldGroupCollapse (type)", "FieldGroupSection": "src/data/field-group-layout.ts#FieldGroupSection (interface)", + "FieldGroupVisibleWhen": "src/data/field-group-layout.ts#FieldGroupVisibleWhen (type)", "FieldInput": "src/data/field.zod.ts#FieldInput (type)", "FieldMaskingKeep": "src/data/field.zod.ts#FieldMaskingKeep (type)", "FieldMaskingKeepSchema": "src/data/field.zod.ts#FieldMaskingKeepSchema (const)",