Skip to content

finding(fields): RichTextField reads a rows key off field metadata that neither MarkdownFieldMetadata nor HtmlFieldMetadata declares — undeclared-but-consumed, reached through an as any #6140

Description

@yinlianghui-tw

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.mdxread by RichTextField?evidence
rowsYESconst rows = richField?.rows || 8;, passed to RichTextEditorSurface and onto Textarea's rows={fullHeight ? undefined : rows}
toolbarnothe 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)"
previewnono read on this widget; the other preview hits in packages/fields/src belong to VectorField, RecordPickerDialog and ImageLightbox
minHeightnozero occurrences anywhere in packages/fields/src
maxHeightnozero 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 them
  • HtmlFieldMetadata — 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:

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepm:queue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions