Skip to content

finding(core): rowHeightToDensityMode coerces an off-spec rowHeight to comfortable, the opposite of what the spec bridge now does with the same input #4440

Description

@yinlianghui

Found while implementing #4352 (PR #4439). Observation-class: nothing a user hits today, and no fix is proposed here.

What

Two surfaces narrow a list view's rowHeight onto the renderer's three-step density vocabulary, and after #4352 they answer differently for the same off-spec input.

packages/core/src/utils/normalize-list-view.ts:62:

export function rowHeightToDensityMode(rowHeight: unknown): DensityMode {
  if (typeof rowHeight === 'string' && rowHeight in ROW_HEIGHT_TO_DENSITY_MODE) {
    return ROW_HEIGHT_TO_DENSITY_MODE[rowHeight as RowHeight];
  }
  return 'comfortable';
}

Its doc comment states the intent plainly: "View metadata is user-authored, so rowHeight is not guaranteed to be one of the five spec values — an unknown one lands on comfortable rather than rendering an undefined density."

packages/react/src/spec-bridge/bridges/list-view.ts, after #4352, takes the other branch of the same question: an off-spec rowHeight yields no density at all, so the renderer's own default applies. That was the #4352 ruling, on AGENTS.md #0.1 grounds — one strict contract beats N dialects, and a bad rowHeight gets fixed at the producer.

So @object-ui/plugin-list's ListView (the rowHeightToDensityMode caller, ListView.tsx:877) resolves a garbage rowHeight to comfortable, while the spec bridge resolves the same string to nothing. Neither is obviously wrong in isolation; what is odd is that one metadata-driven system holds both answers.

Why it is observation-class and not a defect today

The empirical sweep run for #4352 found zero authored off-spec rowHeight values anywhere reachable: this repo (examples/, apps/ including the console's view metadata, all packages/, every JSON/YAML), the objectstack example apps (showcase and CRM), and one real downstream app. Every authored value is one of the five RowHeightSchema admits. The legacy densityMode alias cannot produce an off-spec value either — DENSITY_MODE_TO_ROW_HEIGHT is typed Record< DensityMode, RowHeight >, so its codomain is compact / medium / tall.

The divergence is therefore reachable only by metadata that does not exist, which is exactly why #4352 could delete its four dead spellings without a conversion card.

The decision, when someone takes it

Two readings, and they are not equally cheap:

  1. Align on strictness — drop the 'comfortable' fallback so an off-spec rowHeight resolves to nothing on both surfaces, matching #0.1. Note this fallback is not the same shape as finding(react): bridgeListView's mapDensity carries four rowHeight spellings no spec-valid list view can hold #4352's dead keys: it is a catch-all, not four named aliases, so it does not fossilize any particular misspelling into a second vocabulary. It also affects a live render path rather than an unreachable one, so unlike finding(react): bridgeListView's mapDensity carries four rowHeight spellings no spec-valid list view can hold #4352 it would want its own empirical check on what ListView actually receives at runtime, including from stored per-user view overrides.
  2. Leave it and write down why — the two surfaces genuinely differ in position: the bridge is a translation boundary that can decline to answer, while rowHeightToDensityMode returns a non-optional DensityMode and has no "no answer" to give without changing its signature. If that asymmetry is the intended design, the doc comment should say so and name the bridge's opposite choice, so the next reader does not file this twice.

Worth deciding deliberately rather than by whichever surface someone reads first.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions