Filed unassigned from objectui#6138 round 2, which ruled that rich-text.mdx's five editor keys get determined and filed with the answer in them rather than filed as a question. The answer is measured below. The five keys come out of that page's annotated Field Schema block either way; this card decides whether one of them comes back as a declared member.
The measurement — one of five is consumed, four are inert
Read: packages/fields/src/widgets/RichTextField.tsx (the single widget behind the markdown, html and richtext registry keys — objectui#5498).
key documented on rich-text.mdx | read by RichTextField? | evidence |
|---|
rows | YES | const rows = richField?.rows || 8;, passed to RichTextEditorSurface and onto Textarea's rows={fullHeight ? undefined : rows} |
toolbar | no | the only occurrence in packages/fields/src is a JSDoc sentence about what the editor might grow into — "whatever the editor grows into (a toolbar, a TipTap/Lexical instance, a preview toggle)" |
preview | no | no read on this widget; the other preview hits in packages/fields/src belong to VectorField, RecordPickerDialog and ImageLightbox |
minHeight | no | zero occurrences anywhere in packages/fields/src |
maxHeight | no | zero occurrences anywhere in packages/fields/src |
Command:
grep -rn "\btoolbar\b\|\bpreview\b\|\bminHeight\b\|\bmaxHeight\b\|\brows\b" \
packages/fields/src --include=*.ts --include=*.tsx | grep -v __tests__
So four of the five are inert prose and are simply deleted by objectui#6138. rows is the finding.
Why rows is a defect rather than a working feature
The read is real and reaches the DOM, but it is undeclared on both metadata types the widget can be handed:
MarkdownFieldMetadata — 18 members, rows not among themHtmlFieldMetadata — 18 members, rows not among them
(Key sets read from the builtpackages/types/dist/index.d.ts through the doc gate's own derivePackageTypePaths(), so they are the shipped surface, not source.)
The read only compiles because the widget launders the carrier first:
const richField = field as any;
const rows = richField?.rows || 8;
field is typed FieldMetadata, so without that as any the read would not compile. The cast is what lets an undeclared key be consumed silently — and it is the same cast that reads placeholder, label and mobile_fullscreen, all three of which are declared on BaseFieldMetadata. rows is the one member of that group with no declaration behind it.
Consequences, both live today:
- An author cannot legally write it. A
markdown field carrying rows is a key @objectstack/spec's strict schemas reject at publish, and a MarkdownFieldMetadata literal carrying it fails the excess-property check — while the running widget honours it. Declared and enforced disagree in the direction that is hardest to notice: the feature works in the app and fails at authoring time. - The sibling proves the shape.
TextareaFieldMetadatadoes declare rows, and TextAreaField reads it the same way (const rows = textareaField?.rows || 4;). Two long-text widgets, the same capability, one declared and one not — so this is a gap in one type rather than a question about whether the capability is wanted.
The two candidate resolutions
- A — declare it. Add
rows?: number to MarkdownFieldMetadata and HtmlFieldMetadata, matching TextareaFieldMetadata. Makes the consumed key authorable, and rich-text.mdx's Field Schema block can then carry it. Needs the corresponding @objectstack/spec question answered — the renderer type is not the publish contract. - B — remove the read. Drop
richField?.rows and let the inline editor keep its fixed height. Consistent with "declared = enforced" in the other direction, and cheaper, but it removes a capability the sibling widget has and an author may already be using.
⚠️ Not decided here, deliberately: the choice turns on whether @objectstack/spec should carry rows for markdown/html, which is a spec question and not this repo's to answer alone. What this card fixes is that the question was previously invisible.
Refs
objectui#6138 (which ruled this be filed with its answer) · objectui#5498 (the one widget, three registry keys) · objectui#3233 (field as the single metadata carrier) · objectui#3221 (why the closed contract matters).
Generated by Claude Code
Filed unassigned from objectui#6138 round 2, which ruled that
rich-text.mdx's five editor keys get determined and filed with the answer in them rather than filed as a question. The answer is measured below. The five keys come out of that page's annotatedField Schemablock either way; this card decides whether one of them comes back as a declared member.The measurement — one of five is consumed, four are inert
Read:
packages/fields/src/widgets/RichTextField.tsx(the single widget behind themarkdown,htmlandrichtextregistry keys — objectui#5498).rich-text.mdxRichTextField?rowsconst rows = richField?.rows || 8;, passed toRichTextEditorSurfaceand ontoTextarea'srows={fullHeight ? undefined : rows}toolbarpackages/fields/srcis a JSDoc sentence about what the editor might grow into — "whatever the editor grows into (a toolbar, a TipTap/Lexical instance, a preview toggle)"previewpreviewhits inpackages/fields/srcbelong toVectorField,RecordPickerDialogandImageLightboxminHeightpackages/fields/srcmaxHeightpackages/fields/srcCommand:
So four of the five are inert prose and are simply deleted by objectui#6138.
rowsis the finding.Why
rowsis a defect rather than a working featureThe read is real and reaches the DOM, but it is undeclared on both metadata types the widget can be handed:
MarkdownFieldMetadata— 18 members,rowsnot among themHtmlFieldMetadata— 18 members,rowsnot among them(Key sets read from the built
packages/types/dist/index.d.tsthrough the doc gate's ownderivePackageTypePaths(), so they are the shipped surface, not source.)The read only compiles because the widget launders the carrier first:
fieldis typedFieldMetadata, so without thatas anythe read would not compile. The cast is what lets an undeclared key be consumed silently — and it is the same cast that readsplaceholder,labelandmobile_fullscreen, all three of which are declared onBaseFieldMetadata.rowsis the one member of that group with no declaration behind it.Consequences, both live today:
markdownfield carryingrowsis a key@objectstack/spec's strict schemas reject at publish, and aMarkdownFieldMetadataliteral carrying it fails the excess-property check — while the running widget honours it. Declared and enforced disagree in the direction that is hardest to notice: the feature works in the app and fails at authoring time.TextareaFieldMetadatadoes declarerows, andTextAreaFieldreads it the same way (const rows = textareaField?.rows || 4;). Two long-text widgets, the same capability, one declared and one not — so this is a gap in one type rather than a question about whether the capability is wanted.The two candidate resolutions
rows?: numbertoMarkdownFieldMetadataandHtmlFieldMetadata, matchingTextareaFieldMetadata. Makes the consumed key authorable, andrich-text.mdx'sField Schemablock can then carry it. Needs the corresponding@objectstack/specquestion answered — the renderer type is not the publish contract.richField?.rowsand let the inline editor keep its fixed height. Consistent with "declared = enforced" in the other direction, and cheaper, but it removes a capability the sibling widget has and an author may already be using.@objectstack/specshould carryrowsformarkdown/html, which is a spec question and not this repo's to answer alone. What this card fixes is that the question was previously invisible.Refs
objectui#6138 (which ruled this be filed with its answer) · objectui#5498 (the one widget, three registry keys) · objectui#3233 (
fieldas the single metadata carrier) · objectui#3221 (why the closed contract matters).Generated by Claude Code