The legal primitive for each widget — the whitelist of pairs, expressed as a
type so TypeScript refuses an illegal couple at the same place the JSON schema
refuses it at build time.
checkbox holds a boolean when single and a string[] when its multiple
option is set.
action takes "string" because its declared field still names a property —
the one a host may key its handler on. The button's identity travels in
options.actionId, which is opaque to the core.
⚠️ badge, link and price accept BOTH a scalar and an object, and the reason
is measured rather than defensive. The first cut of this contract derived them
from each field-renderer component's ComponentDefinition<TValue> — that is,
from the CAPTURE side, where a badge really is {label, color}. On the READING
side the value is a plain string: renderBadge takes the label and gets its
colours from the taxonomy seam, renderLink takes a URL. Confronting the
declaration against the actual GeoJSON of the shipped profiles, on 50 features per
layer, found 15 fields on 86 where the object form would have been a false
declaration — and primitive is defined as what the value IS in the feature.
price has no user in any profile today; it is widened with the other two because
it belongs to the same group and its formatter carries the same scalar branch.
Declaring it narrowly would leave the next price field to rediscover this.
The legal
primitivefor eachwidget— the whitelist of pairs, expressed as a type so TypeScript refuses an illegal couple at the same place the JSON schema refuses it at build time.checkboxholds abooleanwhen single and astring[]when itsmultipleoption is set.actiontakes"string"because its declaredfieldstill names a property — the one a host may key its handler on. The button's identity travels inoptions.actionId, which is opaque to the core.⚠️
badge,linkandpriceaccept BOTH a scalar and an object, and the reason is measured rather than defensive. The first cut of this contract derived them from eachfield-renderercomponent'sComponentDefinition<TValue>— that is, from the CAPTURE side, where a badge really is{label, color}. On the READING side the value is a plain string:renderBadgetakes the label and gets its colours from the taxonomy seam,renderLinktakes a URL. Confronting the declaration against the actual GeoJSON of the shipped profiles, on 50 features per layer, found 15 fields on 86 where the object form would have been a false declaration — andprimitiveis defined as what the value IS in the feature.pricehas no user in any profile today; it is widened with the other two because it belongs to the same group and its formatter carries the same scalar branch. Declaring it narrowly would leave the nextpricefield to rediscover this.