Filed from objectui#5183's implementation as the residual half of that card's design question. Not a defect in shipped behaviour — after #5183 every current call site resolves correctly. This is about the next one.
The shape of the trap
@object-ui/fields exports two helpers that answer the same question in incompatible currencies:
getBadgeColorClasses(color, val) returns a class string — complete-looking, and unable to carry a runtime colour.getBadgeHexAppearance(color) returns { className, style } or undefined — the correct answer, but only if the caller remembers to consult it first and carries the style through.
Every badge surface that reaches for the first one alone quantizes an author-declared hex onto one of nine palette families. That is exactly what objectui#5141 fixed in the cell renderer and objectui#5183 fixed at four more sites. Nothing prevents a sixth.
The failure mode is quiet in both directions: the class-only call compiles, renders, and looks right for family-name declarations (the common case), so it fails only for authors who declared a hex — and it fails by rendering a plausible neighbouring colour rather than by breaking.
Why objectui#5183 did not close it
The dispatch for #5183 asked whether the four sites should route through one shared helper instead of adopting the pair individually. Measured there, and recorded so this is not re-derived:
- No shared home was in surface. The only package both
plugin-grid and plugin-kanban depend on that owns the palette is @object-ui/fields, which was held out of that round; a resolver there is also a new public export on a published package. - One resolver cannot serve all four unchanged.
ObjectGrid's compact-card badge falls back to a local pipeline-stage heuristic (stageBadgeColor) when no colour is declared, not to getBadgeColorClasses. A (color, value) resolver would have to grow a per-site fallback argument, which puts the divergence back into a parameter. - A resolver would not remove the trap anyway.
getBadgeColorClasses remains exported and class-only, so the next caller can still reach for it. Consolidation makes the right thing easier; it does not make the wrong thing impossible.
Options, roughly ordered by how mechanically they close it
- A lint rule (this repo already ships local ratchet rules under
eslint-rules/) that flags a getBadgeColorClasses call in a package outside fields unless the same scope also consults getBadgeHexAppearance. Closes the class structurally, no published-surface change. - A consolidated export — e.g.
getBadgeAppearance(color, value, fallbackClassName?) returning { className, style? } — with getBadgeColorClasses documented as the family-name half of it. Widens the published surface; needs the fallback parameter for the stage-heuristic site. - Both, with the lint rule pointing at the new export.
- Nothing, and accept that each new badge surface re-derives the pattern from a sibling call site. That is the status quo and is a legitimate answer for a surface that grows this slowly — three surfaces in the last two cards.
Recommendation: the lint rule. It is the only option that makes the fifth caller's mistake impossible rather than merely documented, and it costs no published surface.
What #5183 did instead
Both carriers that had to widen (GroupRow.labelColorStyle, KanbanCard.badges[].colorStyle) carry doc comments stating that the style is required whenever the class came from getBadgeHexAppearance, and two cross-surface tests pin that the surfaces agree. That is documentation plus a regression pin — it catches a regression on the surfaces that exist, not a new surface that forgets.
Related: objectui#5141, objectui#5183.
Filed from objectui#5183's implementation as the residual half of that card's design question. Not a defect in shipped behaviour — after #5183 every current call site resolves correctly. This is about the next one.
The shape of the trap
@object-ui/fieldsexports two helpers that answer the same question in incompatible currencies:getBadgeColorClasses(color, val)returns a class string — complete-looking, and unable to carry a runtime colour.getBadgeHexAppearance(color)returns{ className, style }orundefined— the correct answer, but only if the caller remembers to consult it first and carries thestylethrough.Every badge surface that reaches for the first one alone quantizes an author-declared hex onto one of nine palette families. That is exactly what objectui#5141 fixed in the cell renderer and objectui#5183 fixed at four more sites. Nothing prevents a sixth.
The failure mode is quiet in both directions: the class-only call compiles, renders, and looks right for family-name declarations (the common case), so it fails only for authors who declared a hex — and it fails by rendering a plausible neighbouring colour rather than by breaking.
Why objectui#5183 did not close it
The dispatch for #5183 asked whether the four sites should route through one shared helper instead of adopting the pair individually. Measured there, and recorded so this is not re-derived:
plugin-gridandplugin-kanbandepend on that owns the palette is@object-ui/fields, which was held out of that round; a resolver there is also a new public export on a published package.ObjectGrid's compact-card badge falls back to a local pipeline-stage heuristic (stageBadgeColor) when no colour is declared, not togetBadgeColorClasses. A(color, value)resolver would have to grow a per-site fallback argument, which puts the divergence back into a parameter.getBadgeColorClassesremains exported and class-only, so the next caller can still reach for it. Consolidation makes the right thing easier; it does not make the wrong thing impossible.Options, roughly ordered by how mechanically they close it
eslint-rules/) that flags agetBadgeColorClassescall in a package outsidefieldsunless the same scope also consultsgetBadgeHexAppearance. Closes the class structurally, no published-surface change.getBadgeAppearance(color, value, fallbackClassName?)returning{ className, style? }— withgetBadgeColorClassesdocumented as the family-name half of it. Widens the published surface; needs the fallback parameter for the stage-heuristic site.Recommendation: the lint rule. It is the only option that makes the fifth caller's mistake impossible rather than merely documented, and it costs no published surface.
What #5183 did instead
Both carriers that had to widen (
GroupRow.labelColorStyle,KanbanCard.badges[].colorStyle) carry doc comments stating that the style is required whenever the class came fromgetBadgeHexAppearance, and two cross-surface tests pin that the surfaces agree. That is documentation plus a regression pin — it catches a regression on the surfaces that exist, not a new surface that forgets.Related: objectui#5141, objectui#5183.