Skip to content

Commit 2af790f

Browse files
spec(ui): record:details hideEmpty: false — in read-only mode an all-empty section renders its heading plus one fold row; describe() says so (#25612)
Fixes #25511 Clause-②: no Metadata-protocol: semantic ## What this changes The `record:details` section key `hideEmpty` (`packages/spec/src/ui/component.zod.ts`, the `RecordDetailsProps.sections[]` object form) gets a new `describe()` text. It implements ruling C on objectstack-ai/objectui#13386 (batch #7 item 4, the maintainer's 「objectui 三个都同意」, 2026-10-23T16:08:53Z): - **Default (key omitted, or `true`): unchanged.** An all-empty section renders nothing at all: no heading, no skeleton. - **`false`, read-only mode:** an all-empty section renders its heading plus one fold row that counts the empty fields and reveals them on request. This is the same fold a partly-filled section uses. - **`false`, edit mode:** unchanged. Every field renders as an editable row. No key, shape or default changes. The renderer change is objectui's (`DetailSection`); objectstack-ai/objectui#13386 carries `Blocked-by:` this card and remains open. | | Before | After | |---|---|---| | `describe()` | Hide this section's empty fields (renderer default: on — and a section whose fields are ALL empty then renders nothing at all: no heading, no skeleton). Set `false` to render empty rows, keeping the section's label skeleton on an all-empty record (e.g. a brand-new one). | Whether this section disappears when ALL its fields are empty (renderer default: on — it then renders nothing at all: no heading, no skeleton). Set `false` to keep it on an all-empty record (e.g. a brand-new one): in read-only mode it renders its heading plus one fold row that counts the empty fields and reveals them on request — the same fold a partly-filled section uses; in edit mode it renders every field as an editable row. | Files: - `packages/spec/src/ui/component.zod.ts`: the describe string, plus one docblock paragraph above it recording the re-ruled `false` rendering and that the key governs only the all-empty case. The docblock's existing text is about the default and the pre-declaration history, so it stays as it was. - `content/docs/references/ui/component.mdx`: regenerated with `gen:docs`. `check:generated` named it as the only stale artifact. The shipped `json-schema/ui/RecordDetailsProps.json` (gitignored, in `files[]`) carries the new text after a build. - `.changeset/25511-hide-empty-describe.md`: `@objectstack/spec` `minor`, with both declaration lines. Landing: the measured producer is the one the card named, `component.zod.ts` at `:1440`. No other producer of this text exists (`git grep` for the old phrases finds only the generated mdx, a test comment and the release-owned `CHANGELOG.md`). ## Two wording calls 1. **The fold label is not quoted.** The card writes the row as "N empty fields · Show". The ruling's text is 「N 个空字段 · 展开」, and what it rules is "the same fold the mixed sections use". objectui's English label for that fold is "Show {{count}} empty fields" (`detail.showEmptyFields`, `packages/i18n/src/locales/en.ts` at objectui `7981384`). Quoting "N empty fields · Show" would contradict "the same fold", and quoting objectui's string would pin a label the spec does not own. So the text says what the row does: it counts the empty fields and reveals them on request. 2. **The lead changed from "Hide this section's empty fields" to "Whether this section disappears when ALL its fields are empty".** Under C, both values hide an all-empty section's empty rows in read-only mode (`true` drops the section, `false` folds the rows), so the old lead no longer told the two values apart. The key never governed a partly-filled section's empty rows either: the renderer's own fold handles those whatever the key says (objectui#7129 Q2-C). The default sentence keeps its meaning; only its grammar follows the new lead. ## Verification All readings were taken on head `0ed74f8840` (the merge of `origin/main` `e649c2d91e`; the branch delta against `origin/main` is exactly the three files above). - `pnpm --filter @objectstack/spec build`: lock VERDICT command-exit 0. The build left no tracked file changed (`authorable-surface.base.json` untouched). - `pnpm --filter @objectstack/spec check:generated`, before regeneration: exit 1, `✗ 1 of 14 artifact(s) stale: content/docs/references/**`. After `gen:docs`, on the final head: `✓ All 14 generated artifacts are up to date`. - `pnpm --filter @objectstack/spec test`: `Test Files 714 passed (714)`, `Tests 21687 passed | 1 todo (21688)`. `pnpm --filter @objectstack/spec typecheck` (tsc, scripts typecheck, test typecheck): exit 0. Lock VERDICT command-exit 0. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 107 commands from this diff. Each was run with its exit code captured before any pipe: 106 exited 0. One, `pnpm check:dual-build-cjs-loads`, exited 3 (`PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/`), so it is NOT MEASURED. It needs every workspace package built, and only the spec, lint, objectql, client, client-react and formula closures were built (targeted build; declared narrowing). CI's `Lint & Repo Gates` runs it. Reconciliation: `✓ dispatch-gates --ran: 107 derived famil(ies) accounted for — 106 run, 1 NOT-MEASURED (0 DERIVED from a recorded exit 3, 1 claimed).` Selected verdict lines: `check:generated` `✓ All 14 generated artifacts are up to date`; `check:docs` `✅ 227 generated files in sync with packages/spec`; `check:api-surface` `public API surface + factory signatures unchanged ✓`; `check:doc-authoring` clean (9559 describe strings read); `check:nul-bytes` OK; `check-adr-0087-registration` `this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)`. - Local-only `check:dev-prereqs`: its self-test passes, but the scan exits 1 with `✗ The workspace is not built — 1 unmet precondition` (33 of 70 packages have no `dist/`, same targeted-build cause). NOT MEASURED. The spec build's own stamp (`✓ packages/spec/dist/.build-input-hash`) and `check:generated`'s declaration-stamp match show `packages/spec/dist` is current. - No ablation or reverse verification. The diff changes describe text and generated docs only, and no test pins that text. ## Acceptance notes - **The renderer lags the text until objectstack-ai/objectui#13386 lands.** Until then, a `false` all-empty section still renders empty rows in read-only mode. The ruling orders it this way (spec text first, renderer second). - **The default in edit mode** (noted, not filed; carrier: none). objectui renders every row of an all-empty section in edit mode for the default too (`hideAllEmptySection` requires `!isEditing`, `DetailSection.tsx` at objectui `7981384`). The default sentence says "renders nothing at all" with no mode qualifier, as the card asked. No author writes wrong metadata because of it. - **A stale test comment** (noted, not filed; carrier: none). `packages/spec/src/ui/component.test.ts` at about line 636 still calls `false` "the only spelling that keeps a section's label skeleton on an all-empty record". That file is outside the claim's file surface, and no assertion depends on the wording. --- _Generated by [Claude Code](https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7b0298 commit 2af790f

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
spec(ui): `record:details` section `hideEmpty: false` — in read-only mode an all-empty section renders its heading plus one fold row, and the `describe()` text says so
6+
7+
Clause-②: no
8+
Metadata-protocol: semantic
9+
10+
The meaning of `hideEmpty: false` on a `record:details` section changes for a section whose fields are ALL empty (for example on a brand-new record):
11+
12+
- **Read-only mode:** the section renders its heading plus one fold row that counts the empty fields and reveals them on request — the same fold a partly-filled section already uses. It no longer renders a column of empty rows.
13+
- **Edit mode:** unchanged. Every field renders as an editable row.
14+
15+
Nothing else changes. No key is added, removed or renamed; the shape and the default are the same. Leaving `hideEmpty` out (or writing `true`) still means an all-empty section renders nothing at all: no heading, no skeleton. A partly-filled section's empty rows are not governed by this key, in either value. Authored metadata needs no change: a section that writes `hideEmpty: false` to keep its heading on an empty record keeps it.
16+
17+
The `describe()` text, which the reference docs and the JSON Schema read, now states this. Its lead changes from "Hide this section's empty fields" to "Whether this section disappears when ALL its fields are empty", because under the new reading both values hide an all-empty section's empty rows in read-only mode and the old lead no longer told them apart. The renderer change is in objectui's record detail section.

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1697,7 +1697,7 @@ View filter rule
16971697
| **columns** | `integer` | optional | Field-grid columns for this section (1-4). Omitted → the renderer derives the width. |
16981698
| **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's `fieldGroups` (ADR-0085 §5 `deriveFieldGroupLayout`). Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `icon`, `description`, `collapsible`, `defaultCollapsed`). Must name a declared group — checked by reference diagnostics. |
16991699
| **fields** | `string[]` | optional | Field names rendered in this section, in order. Omit only when `group` supplies the members instead. |
1700-
| **hideEmpty** | `boolean` | optional | Hide this section's empty fields (renderer default: on — and a section whose fields are ALL empty then renders nothing at all: no heading, no skeleton). Set `false` to render empty rows, keeping the section's label skeleton on an all-empty record (e.g. a brand-new one). |
1700+
| **hideEmpty** | `boolean` | optional | Whether this section disappears when ALL its fields are empty (renderer default: on — it then renders nothing at all: no heading, no skeleton). Set `false` to keep it on an all-empty record (e.g. a brand-new one): in read-only mode it renders its heading plus one fold row that counts the empty fields and reveals them on request — the same fold a partly-filled section uses; in edit mode it renders every field as an editable row. |
17011701
| **collapsible** | `boolean` | optional | Render this section as a collapsible card — the heading becomes a chevron toggle, initially expanded (renderer default: off). |
17021702
| **showBorder** | `boolean` | optional | Draw this section's card chrome (renderer default: derived — on for a titled section, off for an untitled one). Set `false` for a borderless titled section, or `true` for a bordered untitled one. |
17031703
| **defaultCollapsed** | `boolean` | optional | Start a `collapsible: true` section collapsed (renderer default: expanded). Consulted only when `collapsible` is on — a non-collapsible section never reads its collapse state. |

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1436,8 +1436,19 @@ export const RecordDetailsProps = strictObject({
14361436
* asked for the default". Defaults in the describe() texts are MEASURED on
14371437
* the renderer (read points recorded in `liveness/page.json`, row
14381438
* `regions`), not transcribed from a TS interface.
1439+
*
1440+
* What `hideEmpty: false` renders was re-ruled (ruling C on
1441+
* objectstack-ai/objectui#13386, maintainer 2026-10-23): in read-only mode
1442+
* an all-empty section renders its heading plus ONE fold row — the same
1443+
* fold a partly-filled section uses — instead of a column of empty rows;
1444+
* edit mode is unchanged and renders every row, editable. No key, shape or
1445+
* default changed, and the describe() states the ruled rendering, which
1446+
* objectui's `DetailSection` follows. The key governs only the ALL-empty
1447+
* case: a partly-filled section's empty rows follow the renderer's own
1448+
* fold (objectui#7129 Q2-C) in either polarity, so the old lead "hide this
1449+
* section's empty fields" no longer told the two values apart.
14391450
*/
1440-
hideEmpty: z.boolean().optional().describe('Hide this section\'s empty fields (renderer default: on — and a section whose fields are ALL empty then renders nothing at all: no heading, no skeleton). Set `false` to render empty rows, keeping the section\'s label skeleton on an all-empty record (e.g. a brand-new one).'),
1451+
hideEmpty: z.boolean().optional().describe('Whether this section disappears when ALL its fields are empty (renderer default: on — it then renders nothing at all: no heading, no skeleton). Set `false` to keep it on an all-empty record (e.g. a brand-new one): in read-only mode it renders its heading plus one fold row that counts the empty fields and reveals them on request — the same fold a partly-filled section uses; in edit mode it renders every field as an editable row.'),
14411452
/** Collapsible card. Initial state is expanded; the toggle is the heading. */
14421453
collapsible: z.boolean().optional().describe('Render this section as a collapsible card — the heading becomes a chevron toggle, initially expanded (renderer default: off).'),
14431454
/** Card chrome; the renderer derives it from the presence of a title. */

0 commit comments

Comments
 (0)