@object-ui/fields 17.5.0 → 17.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/dist/FieldEditWidget.d.ts +16 -2
  3. package/dist/field-type-alias.d.ts +16 -0
  4. package/dist/index.cjs +4 -4
  5. package/dist/index.css +27 -9
  6. package/dist/index.d.ts +425 -72
  7. package/dist/index.js +2530 -2228
  8. package/dist/widgets/AddressField.d.ts +3 -3
  9. package/dist/widgets/AutoNumberField.d.ts +1 -1
  10. package/dist/widgets/AvatarField.d.ts +1 -1
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/DateField.d.ts +1 -1
  18. package/dist/widgets/DateTimeField.d.ts +1 -1
  19. package/dist/widgets/EmailField.d.ts +1 -1
  20. package/dist/widgets/FileField.d.ts +18 -2
  21. package/dist/widgets/FilterConditionField.d.ts +28 -1
  22. package/dist/widgets/FormulaField.d.ts +1 -1
  23. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  24. package/dist/widgets/GeolocationField.d.ts +1 -1
  25. package/dist/widgets/GridField.d.ts +56 -4
  26. package/dist/widgets/ImageField.d.ts +1 -1
  27. package/dist/widgets/LocationField.d.ts +1 -1
  28. package/dist/widgets/LookupField.d.ts +1 -1
  29. package/dist/widgets/MasterDetailField.d.ts +1 -1
  30. package/dist/widgets/MultiSelectField.d.ts +1 -1
  31. package/dist/widgets/NumberField.d.ts +1 -1
  32. package/dist/widgets/ObjectField.d.ts +1 -1
  33. package/dist/widgets/ObjectRefField.d.ts +1 -1
  34. package/dist/widgets/OptionsEmptyState.d.ts +13 -7
  35. package/dist/widgets/PasswordField.d.ts +1 -1
  36. package/dist/widgets/PercentField.d.ts +1 -1
  37. package/dist/widgets/PhoneField.d.ts +1 -1
  38. package/dist/widgets/QRCodeField.d.ts +1 -1
  39. package/dist/widgets/RadioField.d.ts +1 -1
  40. package/dist/widgets/RatingField.d.ts +1 -1
  41. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  42. package/dist/widgets/RecordPickerDialog.d.ts +2 -4
  43. package/dist/widgets/RichTextField.d.ts +37 -1
  44. package/dist/widgets/SelectField.d.ts +1 -1
  45. package/dist/widgets/SignatureField.d.ts +32 -2
  46. package/dist/widgets/SliderField.d.ts +37 -2
  47. package/dist/widgets/SummaryField.d.ts +1 -1
  48. package/dist/widgets/TagsField.d.ts +1 -1
  49. package/dist/widgets/TextAreaField.d.ts +6 -2
  50. package/dist/widgets/TextField.d.ts +1 -1
  51. package/dist/widgets/TimeField.d.ts +1 -1
  52. package/dist/widgets/UrlField.d.ts +1 -1
  53. package/dist/widgets/UserField.d.ts +1 -1
  54. package/dist/widgets/VectorField.d.ts +1 -1
  55. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  56. package/dist/widgets/address-format.d.ts +27 -7
  57. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  58. package/dist/widgets/richTextDisplay.d.ts +80 -0
  59. package/dist/widgets/toHostGroupProps.d.ts +119 -21
  60. package/dist/widgets/types.d.ts +8 -3
  61. package/package.json +9 -10
  62. package/dist/widgets/CharacterCount.d.ts +0 -56
  63. package/src/FieldEditWidget.test.ts +0 -133
  64. package/src/FieldEditWidget.tsx +0 -207
  65. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  66. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  67. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  68. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  69. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  70. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  71. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  72. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  73. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  74. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  75. package/src/__tests__/cell-truncation.test.tsx +0 -96
  76. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  77. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  78. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  79. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  80. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  81. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  82. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  83. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  84. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  85. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  86. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  87. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  88. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  89. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  90. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  91. package/src/__tests__/registry-collision.test.ts +0 -91
  92. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  93. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  94. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  95. package/src/__tests__/validation-feedback.test.tsx +0 -361
  96. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  97. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  98. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  99. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  100. package/src/coerce-safe-value.test.ts +0 -34
  101. package/src/complex-widgets.test.tsx +0 -924
  102. package/src/currency.ts +0 -144
  103. package/src/datetime-widgets.test.tsx +0 -185
  104. package/src/field-type-alias.multiple.test.ts +0 -81
  105. package/src/field-type-alias.ts +0 -156
  106. package/src/field-type-coverage.test.ts +0 -67
  107. package/src/index.css +0 -44
  108. package/src/index.tsx +0 -2745
  109. package/src/standard-widgets.test.tsx +0 -364
  110. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  111. package/src/widgets/AddressField.tsx +0 -182
  112. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  113. package/src/widgets/AutoNumberField.tsx +0 -18
  114. package/src/widgets/AvatarField.tsx +0 -117
  115. package/src/widgets/BooleanField.tsx +0 -86
  116. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  117. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  118. package/src/widgets/CharacterCount.tsx +0 -263
  119. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  120. package/src/widgets/CheckboxesField.tsx +0 -169
  121. package/src/widgets/CodeField.tsx +0 -41
  122. package/src/widgets/ColorField.tsx +0 -53
  123. package/src/widgets/CurrencyField.tsx +0 -135
  124. package/src/widgets/DateField.tsx +0 -40
  125. package/src/widgets/DateTimeField.tsx +0 -46
  126. package/src/widgets/EmailField.tsx +0 -37
  127. package/src/widgets/FileField.tsx +0 -512
  128. package/src/widgets/FilterConditionField.tsx +0 -454
  129. package/src/widgets/FormulaField.tsx +0 -38
  130. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  131. package/src/widgets/GeolocationField.tsx +0 -193
  132. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  133. package/src/widgets/GridField.test.tsx +0 -632
  134. package/src/widgets/GridField.tsx +0 -1070
  135. package/src/widgets/ImageCropperDialog.tsx +0 -245
  136. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  137. package/src/widgets/ImageField.tsx +0 -305
  138. package/src/widgets/ImageLightbox.test.tsx +0 -52
  139. package/src/widgets/ImageLightbox.tsx +0 -108
  140. package/src/widgets/LocationField.tsx +0 -57
  141. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  142. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  143. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  144. package/src/widgets/LookupField.idField.test.tsx +0 -61
  145. package/src/widgets/LookupField.tsx +0 -1295
  146. package/src/widgets/MarkdownContent.tsx +0 -19
  147. package/src/widgets/MasterDetailField.tsx +0 -131
  148. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  149. package/src/widgets/MultiSelectField.tsx +0 -196
  150. package/src/widgets/NumberField.tsx +0 -49
  151. package/src/widgets/ObjectField.tsx +0 -94
  152. package/src/widgets/ObjectRefField.tsx +0 -121
  153. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  154. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  155. package/src/widgets/OptionsEmptyState.tsx +0 -99
  156. package/src/widgets/PasswordField.tsx +0 -49
  157. package/src/widgets/PeoplePicker.test.tsx +0 -360
  158. package/src/widgets/PeoplePicker.tsx +0 -579
  159. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  160. package/src/widgets/PercentField.tsx +0 -95
  161. package/src/widgets/PersonRow.tsx +0 -117
  162. package/src/widgets/PhoneField.tsx +0 -37
  163. package/src/widgets/QRCodeField.tsx +0 -103
  164. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  165. package/src/widgets/RadioField.tsx +0 -143
  166. package/src/widgets/RatingField.tsx +0 -94
  167. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  168. package/src/widgets/RecipientPickerField.tsx +0 -188
  169. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  170. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  171. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  172. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  173. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  174. package/src/widgets/RichTextField.tsx +0 -176
  175. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  176. package/src/widgets/SelectField.tsx +0 -193
  177. package/src/widgets/SelectionTray.tsx +0 -110
  178. package/src/widgets/SignatureField.tsx +0 -140
  179. package/src/widgets/SliderField.tsx +0 -39
  180. package/src/widgets/SummaryField.tsx +0 -31
  181. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  182. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  183. package/src/widgets/TagsField.tsx +0 -88
  184. package/src/widgets/TextAreaField.tsx +0 -213
  185. package/src/widgets/TextField.tsx +0 -45
  186. package/src/widgets/TimeField.tsx +0 -31
  187. package/src/widgets/UrlField.tsx +0 -46
  188. package/src/widgets/UserField.tsx +0 -59
  189. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  190. package/src/widgets/VectorField.tsx +0 -29
  191. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  192. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  193. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  194. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  195. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  196. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  197. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  198. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  199. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  200. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  201. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  202. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  203. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  204. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  205. package/src/widgets/address-format.ts +0 -120
  206. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  207. package/src/widgets/deriveLookupColumns.ts +0 -108
  208. package/src/widgets/file-size-guard.ts +0 -55
  209. package/src/widgets/file-value.test.ts +0 -230
  210. package/src/widgets/file-value.ts +0 -246
  211. package/src/widgets/nativeDateValue.test.ts +0 -115
  212. package/src/widgets/nativeDateValue.ts +0 -112
  213. package/src/widgets/openNativePicker.ts +0 -18
  214. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  215. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  216. package/src/widgets/personDisplay.test.ts +0 -113
  217. package/src/widgets/personDisplay.ts +0 -92
  218. package/src/widgets/recentLookups.test.ts +0 -45
  219. package/src/widgets/recentLookups.ts +0 -47
  220. package/src/widgets/toDomProps.ts +0 -204
  221. package/src/widgets/toHostGroupProps.ts +0 -102
  222. package/src/widgets/types.ts +0 -242
  223. package/src/widgets/useCascadingOptions.ts +0 -46
  224. package/src/widgets/useFieldTranslation.ts +0 -132
  225. package/src/widgets/useRecordQuery.test.ts +0 -174
  226. package/src/widgets/useRecordQuery.ts +0 -291
  227. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  228. package/src/widgets/useUploadingSignal.ts +0 -20
  229. package/src/withFieldCarrier.tsx +0 -75
package/CHANGELOG.md CHANGED
@@ -1,5 +1,719 @@
1
1
  # @object-ui/fields
2
2
 
3
+ ## 17.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a09bc33: fix(fields): render the option colour an author declared as an explicit hex, instead of quantizing it to nine palette families (objectui#5141)
8
+
9
+ `options[].color` accepts any hex, but the badge renderer answered a lossy
10
+ question with it: `hexToPaletteName` bucketed the value by hue into nine
11
+ families (`red`/`orange`/`yellow`/`green`/`blue`/`indigo`/`purple`/`pink`, plus
12
+ `gray` below 22% saturation), and `BADGE_COLOR_MAP` held exactly one class set
13
+ per family. Two tiers an author declared as distinct therefore rendered
14
+ byte-identical: `#2ecc71` ("in progress") and `#1e8449` ("completed") differ by
15
+ 0.1 degree of hue, both landed in `green`, and both emitted
16
+ `bg-green-50 text-green-700 border-green-200`. Pressing the second colour darker
17
+ still changed nothing. End users could not tell the two states apart in a list.
18
+
19
+ `plugin-gantt` had already settled this class of conflict the other way —
20
+ *explicit colorField value (hex or semantic name) — metadata wins* — and
21
+ Studio's own option editor paints the author's swatch straight from the raw hex.
22
+ Badges were the odd surface out.
23
+
24
+ Now an explicitly declared hex is rendered as declared: the soft-pill surface,
25
+ label and border are derived from that hex rather than snapped to a family, for
26
+ both `appearance: 'badge'` and `appearance: 'dot'`.
27
+
28
+ Two properties the family maps gave us for free are kept deliberately:
29
+
30
+ - **The design system keeps control of theming.** The derived colours are
31
+ published as CSS custom properties and consumed by *static* Tailwind
32
+ utilities, so light and dark remain ordinary `dark:` variants
33
+ (`.dark\:bg-...:where(.dark, .dark *)` in the built sheet) rather than a
34
+ hard-coded inline background that would render identically in dark mode.
35
+ Tailwind cannot generate a class for a runtime value, so the custom property —
36
+ not the colour — has to be the dynamic part.
37
+ - **Contrast is pinned, not just colour identity.** The label is the lightness
38
+ along the declared hue nearest the declared one that still clears WCAG AA
39
+ (4.5:1) against the surface actually rendered. Authors can and do declare
40
+ colours that are unreadable under a label; honoring the declaration must not
41
+ turn legibility loose across every list view. Dots are held to 1.9:1 against
42
+ the row, the measured floor of the `-500` shades shipped today.
43
+
44
+ **What changes for an author relying on the current look:** every select/status
45
+ badge whose option colour is declared as a hex — which the renderer's own notes
46
+ describe as almost all of them — will render in that declared colour rather than
47
+ its palette family's fixed pill. Colours near a family's canonical shade look
48
+ much as before; a colour the author picked deliberately *away* from it (a deep
49
+ green, a muted red) now looks like what was written, and the pill's depth tracks
50
+ the declared lightness. Declarations that are not an explicit hex are untouched:
51
+ family names, the semantic value map and the deterministic hash fallback all
52
+ resolve exactly as they did, and `getSemanticColorName` still returns family
53
+ names, so the Gantt path is unaffected.
54
+
55
+ The badge classes exported by `getBadgeColorClasses` are unchanged, so callers
56
+ that consume only a class string (the grid's compact card view and group-header
57
+ pills, Kanban) keep today's quantized rendering until they adopt the new
58
+ `getBadgeHexAppearance` / `getDotHexAppearance` helpers.
59
+ - 167ec42: `ComponentMeta.labelling` grows a third value: `'control' | 'group' | 'display'`
60
+ (objectui#4857, ruled jointly with objectui#4871 as the single repo-wide vocabulary for
61
+ "how does a host learn what a widget will render"). `'display'` declares a widget whose
62
+ whole surface is a pure display in EVERY state — no focusable control, nothing a
63
+ `<label for>` could ever reach.
64
+
65
+ The form renderer answers the declaration with the objectui#4788 host container (field
66
+ id + `aria-labelledby` + `aria-describedby` + `role="group"`) in the editable state too;
67
+ the `readonly === true` arm keeps its exact #4788 semantics for undeclared widgets. The
68
+ display-only four (`formula` / `summary` / `auto_number` / `vector`) declare `'display'`
69
+ — on the real object-form path they arrive `disabled`, never `readonly` (a deliberate
70
+ distinction this change does not touch), so their visible labels pointed `for` at an id
71
+ no element carried and their help text had zero consumers in every editable form.
72
+
73
+ `grid` was re-measured before being classified: its only bare-config focusable is the
74
+ auxiliary "Add line" button (routing `for` there would have label clicks insert rows),
75
+ and every realistic config is a table of per-cell inputs — a composite. It declares
76
+ `labelling: 'group'` and its root container now consumes the host id, name and
77
+ description, exactly like `address` / `checkboxes`.
78
+
79
+ Companion registry gate: `FIELD_WIDGET_LABELLING` (exported) is a `Record` keyed by the
80
+ field-widget map's own literal key union, so registering a widget without deciding its
81
+ labelling is a compile error rather than a silent fall-through to the dangling-`for`
82
+ path, and the declaration test asserts the registered meta agrees with it key by key.
83
+
84
+ ### Patch Changes
85
+
86
+ - 69251bf: `AddressField` is translatable, shows no US example placeholders, and formats its readonly line in the reader's address order.
87
+
88
+ The five sub-labels ("Street Address", "City", "State / Province", "ZIP /
89
+ Postal Code", "Country") were English string literals with no i18n key. On a
90
+ non-English console every address field showed five English words in the middle
91
+ of an otherwise fully translated form, and an app had no way to reach them: the
92
+ parts are not fields on the object (`billing_address` is a single `address`
93
+ column), so a translation bundle had nothing to key on, there is no `subLabels`
94
+ property to declare, and the widget cannot be replaced from metadata. They now
95
+ resolve through `fields.address.street` / `.city` / `.state` / `.postalCode` /
96
+ `.country`, added to all ten locale packs. The `en` values are byte-identical to
97
+ the literals they replace, and `FIELD_DEFAULTS` carries the same five, so
98
+ English and provider-less rendering are unchanged.
99
+
100
+ The five input placeholders (`123 Main St`, `San Francisco`, `CA`, `94102`,
101
+ `United States`) are **removed** rather than keyed. They were untranslated and
102
+ US-specific — a zh/ja/ar user was shown an American address as the example of
103
+ what to type — and the right example is a function of the address's country,
104
+ not the reader's language, which no channel in the stored value can supply
105
+ today. Each box keeps the visible label that names it.
106
+
107
+ The readonly line's part order now follows the reader's display locale
108
+ (`useDisplayLocale()`): `zh`, `ja` and `ko` read largest-first (`Country, ZIP
109
+ State, City, Street`), every other locale keeps the unchanged small-to-large
110
+ order (`Street, City, State ZIP, Country`). The display cell renderer takes the
111
+ same locale through the same shared `formatAddress`, so a stored address reads
112
+ identically in a readonly form and in a grid cell.
113
+ - 0ae27f7: The form renderer's built-in `textarea` branch now honours a declared character cap the same way the registered `field:textarea` widget does.
114
+
115
+ One `maxLength` declaration produced two experiences. The registered path has
116
+ shipped four things since objectui#3406/#3408/#3417 — the native cap, visible
117
+ `{n}/{max}` digits, a description reached through `aria-describedby` so the
118
+ limit is announced on focus, and a threshold-gated debounced near-limit notice.
119
+ The built-in branch — the path standalone and embedded hosts take, the ones that
120
+ call no `registerAllFields()` — shipped a subset of one of them.
121
+
122
+ The accessibility half is the half that mattered: a screen-reader user on this
123
+ path learned the field's limit only as a validation error AFTER submitting. All
124
+ four affordances now render on both of the branch's surfaces (the inline control
125
+ and the fullscreen dialog), from the SAME `CharacterCount` component the widget
126
+ renders rather than a second copy of it.
127
+
128
+ Also fixed, and wider than the visible gap: the branch never READ the cap, it
129
+ only spread its leftover field props onto the element. A camelCase `maxLength`
130
+ therefore worked by coincidence — it names a real DOM attribute — while the
131
+ legacy `max_length` spelling, which the registered widget and all three
132
+ producers of a form field have dual-read since framework#1878 §3, landed as a
133
+ stray inert `max_length="…"` attribute and capped nothing at all. The branch now
134
+ resolves both spellings and keeps the non-attribute spelling off the DOM.
135
+
136
+ `CharacterCount` moved from `@object-ui/fields` to `@object-ui/components`, the
137
+ package both render paths may import, in the direction and for the reason
138
+ objectui#3398 measured for `FullscreenEditor`. It was internal to `fields` (never
139
+ exported from that package's barrel), so no published export changed; it is a
140
+ new export of `@object-ui/components`. Its copy moved with it onto the same
141
+ `fields.textarea.*` keys with byte-identical English defaults, so the ten locale
142
+ packs need no edit and provider-less rendering is unchanged.
143
+ - bbe8b86: The allow-list of option widgets that are fed the live record is now one exported constant, `CASCADE_OPTION_WIDGET_TYPES`, instead of three private copies.
144
+
145
+ `select` / `multiselect` / `radio` / `checkboxes` are the widgets whose OFFERED
146
+ option set is re-resolved against a record (per-option `visibleWhen`, plus the
147
+ `dependsOn` gate), so they are the widgets a surface must thread its live record
148
+ to. Three surfaces feed that one evaluator — the object form, the single-record
149
+ action dialog and the bulk action dialog — and until now each carried its own
150
+ private `new Set([...])` of the same four keys, with a comment in each asking the
151
+ next person to change all three together. Nothing could have reported them
152
+ drifting: every copy passed its own behavioural tests, and a divergence would
153
+ have shown up only as one surface silently disagreeing with another about what
154
+ "the record" is.
155
+
156
+ The set now lives in `@object-ui/core`, next to `resolveCascadingOptions` — the
157
+ evaluator that reads that record — because core is the one package all three
158
+ surfaces already depend on, and it is re-exported from `@object-ui/fields` next
159
+ to `resolveFormWidgetType`, whose output is the vocabulary the keys are written
160
+ in. Both are the same object, pinned by test; each consumer keeps its own
161
+ normalization (`normalizeFieldType` in the form, `resolveFormWidgetType` in the
162
+ dialogs), which agree on these four members.
163
+
164
+ No behaviour changes: the members are identical on all three surfaces, and the
165
+ existing pins for each surface still assert the same records reaching the same
166
+ widgets. The rationale that was repeated in the three copies — including the
167
+ note that the widget-hint picker family (`filter-condition`, `recipient-picker`,
168
+ the lookup family) reads a different sibling key off the same channel and is
169
+ deliberately NOT in this set — is now stated once, in the constant's own
170
+ documentation. Whether the action and bulk dialogs should ever feed those
171
+ pickers stays an open question (objectui#4771), unchanged by this convergence.
172
+ - 65e88e6: `@object-ui/fields` stops publishing its `src/` tree
173
+
174
+ The manifest's `files` array listed `src` alongside `dist`, so every published tarball carried all 173 source files — 97 of them `*.test.tsx` / `*.test.ts`. It has been that way since the file's first commit (`780a1b993`), where `files` was already `["dist", "src", "README.md"]` while `exports` named only `dist`, so the entry was never added for a consumer. objectui#4006 recorded this exact shape and did not act on it: its scope was the `*.test.d.ts` half the build program emitted into `dist`, and it noted in passing that the test sources were already in the tarball by this other route.
175
+
176
+ Nothing in the published surface reached those files, which is why no consumer changes in either direction. Measured on a cleanly rebuilt `dist`, all four ways in are closed: the `exports` map has two entries and both target `dist` (`.` resolves `types` / `import` / `require` to `./dist/index.d.ts` / `./dist/index.js` / `./dist/index.cjs`, and `./style.css` to `./dist/index.css`); `main` / `module` / `types` are `./dist/index.cjs`, `./dist/index.js`, `./dist/index.d.ts`; no deep import into this package exists anywhere in the repo or the docs — the one that used to, `@object-ui/fields/widgets/MarkdownContent`, was ruled out by objectui#4325 precisely because a package's surface is its index, and the `../fields/src` paths in sibling `vite.config.ts` files are workspace aliases resolved through `resolve()` against the source tree, which no `files` array shapes; and the tarball holds no sourcemap that could point back at `src`. That last one is the check that had to be measured rather than assumed, because this package emits its declarations through `vite-plugin-dts` rather than the `tsup` of objectui#4847 or the bare `tsc` of `@object-ui/types`: a clean rebuild writes 78 files into `dist`, of which zero are `.d.ts.map`, zero are `.js.map`, zero contain a `sourceMappingURL` comment and zero mention `../src`. `src/index.css` is an input to `scripts/build-css.mjs`, not an output anyone resolves; the sheet the `./style.css` export names is the built `dist/index.css`, which still ships.
177
+
178
+ `npm pack --dry-run` across the change, on the same `dist`:
179
+
180
+ | | before | after |
181
+ | --- | --- | --- |
182
+ | entries | 255 | 82 |
183
+ | unpacked | 2265557 B | 841772 B |
184
+ | tarball | 629843 B | 252364 B |
185
+
186
+ 173 files leave, none arrives, nothing outside `src/` moves, and every surviving entry is byte-identical apart from the edited `package.json` itself. The 173 are the 97 tests plus 76 implementation modules, whose published form remains the bundled `dist/index.js` / `dist/index.cjs` and the 75 declaration files beside them.
187
+
188
+ `@object-ui/types` keeps its `src` entry for now, and that is a different judgement rather than an omission: it builds with a bare `tsc` under a `declarationMap` / `sourceMap` config with no `inlineSources`, so its shipped `dist/*.d.ts.map` name `../src/*.ts` with no embedded content and dropping `src` there would leave published maps pointing at files the tarball no longer carries. That trade-off is filed as objectui#4851.
189
+ - 37f6844: FilterConditionField can author the spec's `$icontains` — case-insensitive contains is reachable from the filter UI.
190
+
191
+ `@objectstack/spec`'s `FieldOperatorsSchema` gained `$icontains` between
192
+ `17.0.0-rc.2` and `rc.5`, and every driver and evaluation face the platform
193
+ ships now executes it. `FilterConditionField` had no builder operator that could
194
+ author it, so the capability was unreachable from the sharing-rule criteria
195
+ builder and sat in that widget's parity test as an explicit `KNOWN_UNREACHABLE`
196
+ entry.
197
+
198
+ The FilterBuilder gains a `containsCaseInsensitive` operator ("Contains (ignore
199
+ case)", translated in all ten locale packs). `condToMongo` emits
200
+ `{ field: { $icontains: value } }` and `kvToCondition` reads it back, so a saved
201
+ criteria reopens in the visual builder instead of falling into the raw-JSON
202
+ editor. Today's `contains` is unchanged and still emits the case-SENSITIVE
203
+ `$contains`; whether it should have been case-insensitive all along is a product
204
+ question that stays open, and stored filter views keep meaning what they meant.
205
+
206
+ The fold is ASCII-only by contract — `café` does not match `CAFÉ`.
207
+
208
+ The new operator is **opt-in per consumer**: `FilterBuilder` takes an
209
+ `extraOperators` prop, and only `FilterConditionField` passes it. The one
210
+ dropdown feeds three at-rest dialects and only the MongoDB-style criteria this
211
+ widget writes can carry the operator — the spec's `VIEW_FILTER_OPERATORS` (saved
212
+ views) and `VALID_AST_OPERATORS` (the live grid's filter AST) have no
213
+ case-insensitive contains, so offering it there would author a filter those
214
+ paths cannot execute. Every other FilterBuilder is unchanged.
215
+ - 911ceaa: The fullscreen long-text dialog announces the field's validation state and carries the field's name
216
+
217
+ objectui#4824, objectui#4832.
218
+
219
+ `mobile.fullscreenLongText` is a shipped opt-in, and with it on the phone user
220
+ edits long text in this dialog and nowhere else. Measured on all three surfaces
221
+ that render the dialog — `TextAreaField`, `RichTextField`, and the form
222
+ renderer's built-in `textarea` branch — with the field genuinely invalid at that
223
+ moment:
224
+
225
+ ```
226
+ INLINE richtext aria-invalid= true
227
+ DIALOG richtext aria-invalid= false aria-describedby= null
228
+ INLINE textarea aria-invalid= true
229
+ DIALOG textarea aria-invalid= null aria-describedby= null
230
+ ```
231
+
232
+ and the accessible name of every dialog control empty, against `F` on every
233
+ inline one. The rich-text row is the sharp half: the dialog was not silent about
234
+ the failure, it was announcing the OPPOSITE of the inline control for the same
235
+ field at the same moment, because `RichTextEditorSurface` computed
236
+ `aria-invalid={!!error}` from an `error` prop the dialog rendering never
237
+ received. 3 surfaces, 3 broken, one cause: the dialog's control is built from
238
+ scratch by the host, so none of the wiring the inline control gets from the form
239
+ renderer reaches it.
240
+
241
+ **Answered once, in the primitive.** `FullscreenEditor` now takes the field's
242
+ `error` and owns what the dialog does with it: it renders the message in a
243
+ dialog-local node, and hands `children` a required fourth argument — a
244
+ spreadable set of DOM attributes — carrying `aria-labelledby` (the dialog
245
+ title's text, i.e. the field label #3393 already put there), `aria-invalid`, and
246
+ `aria-errormessage` naming that node. The host spreads it; the host never learns
247
+ an id, so it cannot name the wrong node, cannot compose the attributes subtly
248
+ wrong, and cannot compute its own `aria-invalid` from a prop it forgot to plumb.
249
+ Three hosts hand-answering this is the shape that produced three identical
250
+ holes.
251
+
252
+ **On objectui#3222's "the text belongs to `FormMessage`".** The maintainer's
253
+ ruling of 2026-08-16 restates that rule as what it was always protecting —
254
+ only one copy of the error text is in the accessibility tree at any moment —
255
+ which the dialog-local node satisfies: it exists only while the dialog is open,
256
+ and for exactly that window Radix `aria-hidden`s everything outside the modal,
257
+ `FormMessage` included. The shortcut of pointing the dialog control's
258
+ `aria-describedby` / `aria-errormessage` at the host's `FormMessage` id is
259
+ forbidden rather than merely unused: it resolves to a node that is `aria-hidden`
260
+ for the whole time the reference is live (an ARIA MUST violation), and neither
261
+ happy-dom nor jsdom can see the difference — which is why every new pin asserts
262
+ that the named node is inside THIS dialog, not merely that it exists.
263
+
264
+ `aria-errormessage` carries a single IDREF and is emitted only alongside
265
+ `aria-invalid="true"`. It is deliberately not folded into the host's
266
+ `aria-describedby` chain, which on the textarea surface already carries the
267
+ fullscreen character counter's sentence.
268
+
269
+ **The name reuses the visible title rather than minting a second author for it**
270
+ (#3978): `aria-labelledby` points at a span inside `DialogTitle`, not at
271
+ `DialogTitle` itself — Radix renders the title as `h2` with the id its own
272
+ `DialogContent` `aria-labelledby` names, so putting an id on it would buy the
273
+ control a name at the cost of the dialog's.
274
+
275
+ **Breaking (shipped as `minor`, see below), `@object-ui/components` only.**
276
+ `FullscreenEditorProps.error` is REQUIRED, not optional, and
277
+ `FullscreenEditorProps['children']` takes a fourth argument.
278
+
279
+ FROM → TO for an out-of-repo host:
280
+
281
+ ```
282
+ <FullscreenEditor value={v} onCommit={c} label={l} testIdPrefix="x">
283
+ {(draft, setDraft, disabled) => <textarea … />}
284
+ </FullscreenEditor>
285
+
286
+ <FullscreenEditor value={v} onCommit={c} label={l} testIdPrefix="x" error={err}>
287
+ {(draft, setDraft, disabled, aria) => <textarea {...aria} … />}
288
+ </FullscreenEditor>
289
+ ```
290
+
291
+ A render prop may still declare fewer parameters, so only `error` fails to
292
+ compile — which is the point of making it required. Every consumer already has
293
+ the value at hand (registered widgets take `error` off the widget props
294
+ contract, objectui#3222; the built-in branch reads `fieldState.error?.message`),
295
+ and an omitted `error` reproduces this defect exactly: a dialog announcing
296
+ `aria-invalid="false"` for a field its own form has already failed. An optional
297
+ key was forgotten by three surfaces in a row; a required one cannot be.
298
+
299
+ `@object-ui/fields` is `patch`: `FullscreenFieldEditor` is internal to that
300
+ package (not re-exported from its entry), so nothing in its public surface
301
+ changes — only the behaviour of the two long-text widgets' dialogs.
302
+
303
+ `minor` rather than `major` follows the repo's standing retirement precedent
304
+ (AGENTS.md §版本号策略, enforced by `scripts/check-changeset-no-major.mjs`): all
305
+ publishable packages sit in one `fixed` group, so a `major` here would carry the
306
+ whole family up against an `@objectstack` that has not moved.
307
+ - 0bffb18: A readonly group-labelled field is now DESCRIBED by its own help text, not just named by its label.
308
+
309
+ The seven `labelling: 'group'` widgets (`address`, `geolocation`, `checkboxes`, `radio`, `rating`, `file`, `multiselect`) render a `<FormDescription>` in their readonly and zero-option states, and the form renderer publishes its `id` — but nothing in the document referenced that id, so the visible help text had no programmatic association with the field it describes. Measured before the change, one field per row: `consumers=0` on every readonly surface of all seven, against `consumers=1` on the same widget's editable one.
310
+
311
+ `toHostGroupProps` now carries `aria-describedby` alongside the host `id` and `aria-labelledby`, onto the `role="group"` surface those states already render (a group is a description carrier under ARIA 1.2 — no focusable control required, which is why this became possible only once objectui#3990 gave those surfaces a role).
312
+
313
+ Control-channel state deliberately does NOT come along: `aria-invalid` and `aria-required` report what a user's editing may do wrong, and a readonly display cannot be edited. The boundary is pinned from both sides, including with a live failed validation on a readonly required field. Editable surfaces are unchanged — a composite's editable container still leaves the description on the sub-input the user focuses, so the help text is never announced twice.
314
+ - 800f455: fix(fields): grid columns are keyed by the declared `name`, so spec-compliant grid metadata renders populated cells
315
+
316
+ `GridField` declared its own column interface keyed by `field` and read
317
+ `c.field` at every site (`key=`, `row[…]`, the blank row, cell writes, the
318
+ column chooser, the running-total lookup), while the published
319
+ `GridColumnDefinition` in `@object-ui/types` — and the grid documentation, and
320
+ the `fields-grid` catalog examples — declare the key as `name`. Metadata
321
+ authored against the published type therefore rendered a grid with the correct
322
+ row count and every cell empty, plus a React "unique key" warning per column.
323
+
324
+ The renderer now reads the declared `name`, and the master-detail derivation
325
+ (`deriveColumns` / `hydrateColumns` / `pickAmountField`) produces and consumes
326
+ the same key. There is deliberately **no** `col.field ?? col.name` alias: one
327
+ spelling at the producer (AGENTS.md #0.1).
328
+
329
+ **Breaking for `field`-keyed columns.** Grid / line-item / master-detail
330
+ subform columns spelled `{ field: 'amount' }` must be re-spelled
331
+ `{ name: 'amount' }`. This affects author-supplied `columns` on the `grid`
332
+ field, `record:line_items`, `object-master-detail-form` details and a
333
+ relationship field's `inlineColumns`. Auto-derived columns (no explicit
334
+ `columns` block) need no change. List-view and `object-grid` columns are a
335
+ different contract (`ListColumn`) and keep their own `field` key.
336
+ - 5458414: Publish relative import specifiers with explicit `.js` extensions so these six packages load under plain Node ESM.
337
+
338
+ Node's ESM resolver does not extension-search relative specifiers and `tsc` never rewrites them, so an extensionless `./Foo` in the source shipped as an extensionless `./Foo` in `dist` and importing the package entry outside a bundler failed with `ERR_MODULE_NOT_FOUND`. Bundled consumers were unaffected. Unbundled consumers — plain Node ESM, an SSR host importing the package directly, anyone running the published tarball without a build step — can now import these entries, and so can the downstream `@object-ui/plugin-*` packages that evaluate through `mobile`, `permissions` and `providers`.
339
+ - 3241559: `object-kanban` now caps its fetch with a real top-level `$top`, and honours `limit`.
340
+
341
+ The board's row cap was written `dataSource.find(objectName, { options: { $top: 100 }, $filter: … })`
342
+ — `$filter` at the top level, where the adapters read it, and the cap one level
343
+ down under `options`, which is not a `QueryParams` key. Nothing in this repo
344
+ reads `params.options` (`convertQueryParams` in `@object-ui/data-objectstack` and
345
+ `ApiDataSource` both read `params.$top`), so the intended cap never reached the
346
+ wire: a board over a large object fetched every row the server would return and
347
+ grouped all of it into lanes client-side. The author's 100 was never live.
348
+
349
+ The cap is now `$top: schema.limit ?? 100`, the shape `object-timeline` took for
350
+ the identical defect. Two consequences worth reading before upgrading:
351
+
352
+ - **A board that used to fetch unbounded now fetches 100 rows by default.** That
353
+ is the previously-declared intent taking effect, not a new limit — but a board
354
+ over an object with more than 100 matching records will show fewer cards than
355
+ it did. Authors who want the old behaviour should declare the window they
356
+ actually want (`limit: 500`), not rely on the absence of one.
357
+ - **`limit` is now mapped from the `dataSource` binding** (`object-kanban`'s
358
+ `ElementDataSourceMapping` gains `limit: 'limit'`). A board bound to a saved
359
+ view is capped by that view's `pagination.pageSize`, and the binding's own
360
+ `limit` overrides it. The flag comes with the read site: it stayed unmapped
361
+ until now on the rationale that the board had a "fixed window", which is the
362
+ claim this change falsifies. `sort` remains unmapped — the board still has no
363
+ `$orderby` read site — and `columns` remains unmapped because a board's columns
364
+ are its swimlanes, not a field projection.
365
+
366
+ `KanbanSchema` gains `limit?: number` (the board's row cap; distinct from a
367
+ column's `limit`, which is that lane's WIP limit and never reaches the query),
368
+ and the guide's per-block table in `content/docs/guide/data-source.md` no longer
369
+ claims a fixed window for `object-kanban`.
370
+
371
+ `@object-ui/fields` carried the same nesting in the lookup-name fallback
372
+ (`find(referenceTo, { $filter: { id }, options: { $top: 1 } })`) where it was
373
+ harmless — a primary-key equality returns at most one row with or without a cap.
374
+ It is spelled `$top: 1` now, so no live instance of the dead nesting is left in
375
+ the repo to read as precedent. No behaviour change there.
376
+ - 616a2a5: The list filter builder no longer offers `Is set` / `Is not set`, which its query dialects cannot express.
377
+
378
+ **User-visible before/after.** The operator dropdown in the list toolbar's
379
+ filter popover — and in the Studio view/tab/page filter inspectors, the dataset
380
+ inspector and the generic `filter` config field — loses two rows: **"Is set"**
381
+ and **"Is not set"**. The sharing-rule criteria builder (`FilterConditionField`)
382
+ keeps both, unchanged. `Is null` / `Is not null` and `Is empty` / `Is not empty`
383
+ are untouched everywhere and remain the way to filter on a missing value from
384
+ the list.
385
+
386
+ Nothing that worked stops working. Every save path behind those two rows was
387
+ already broken, in three different ways depending on the surface:
388
+
389
+ - **Live grid** — `ListView.mapOperator` had no row for either id, so its
390
+ `default:` arm returned the id verbatim and the query went out as
391
+ `['name', 'exists', 'x']`. `exists` is not a member of the spec's
392
+ `VALID_AST_OPERATORS`, so `isFilterAST()` rejects the shape: an unfiltered
393
+ read or a 400, never the filter the user asked for.
394
+ - **Save as view** — `foldFilterGroupToSpecRules` normalizes through the spec's
395
+ `normalizeFilterOperator`, which does not know the pair, and
396
+ `ViewFilterRuleSchema`'s enum then refuses the rule.
397
+ - **Dataset inspector** — `groupToCondition` has no row either and drops the
398
+ condition silently, so the filter simply never applied.
399
+
400
+ **Why withheld rather than mapped.** Measured on `@objectstack/spec`
401
+ 17.0.0-rc.6: neither `VIEW_FILTER_OPERATORS` nor `VALID_AST_OPERATORS` contains
402
+ an existence operator, under any spelling — both sets have zero members matching
403
+ `/exist/`. Only the MongoDB-style `FieldOperatorsSchema` criteria carries
404
+ `$exists`, and that is precisely the dialect `FilterConditionField` writes, so
405
+ the pair moves behind the existing `OPT_IN_OPERATORS` gate and that widget opts
406
+ in. Collapsing them onto `isNotNull` / `isNull` was rejected: the builder
407
+ already draws those as their own rows, the round trip is lossy (a saved
408
+ `exists` reads back as `isNotNull`), and the spec's own note records `$exists` =
409
+ has-value as still unsettled across drivers — `driver-memory`'s live mingo path
410
+ and `driver-mongodb` read key-presence.
411
+
412
+ **The class is now closed by an assertion, not by discipline.** objectui's three
413
+ existing operator-parity guards all sweep spec vocabulary → objectui; none asked
414
+ whether an id the dropdown draws is an id the consumer can persist, which is the
415
+ direction that broke. `plugin-list`'s new
416
+ `list-offered-operator-expressible-parity.test.ts` forces the set the list
417
+ toolbar offers to **equal** the set its two dialects can express, in both
418
+ directions — so an unexpressible operator cannot be offered, and an operator
419
+ that becomes expressible upstream cannot stay needlessly withheld.
420
+ - 6c68b13: Lookup "recently used" now obeys the field's declared filters. The recents rail
421
+ re-fetched its rows without merging either `lookupFilters` or the `dependsOn`
422
+ cascade, so a record the author's metadata excludes stayed visible and
423
+ selectable — pick a product under project A, switch the form to project B, and
424
+ project A's product was still offered. Both re-fetch channels are fixed: the
425
+ inline dropdown (which re-read each remembered id with an unfiltered `findOne`)
426
+ and the search-first picker (which batched recents into an unfiltered `$in`
427
+ query). The currently-selected value still resolves unfiltered, so an existing
428
+ value keeps its label and a selection tray is never silently emptied.
429
+ - 5607092: objectui#4029 — the repo root now lints `no-console` (`error`, allowing
430
+ `warn`/`error`) so a stray module- or function-scope `console.log`/`info`/
431
+ `debug` fails CI instead of shipping silently (as `console.log('Registering
432
+ object-map...')` did in #7139, caught only by hand). Landing the rule meant
433
+ individually judging every real hit outside the tooling exemptions
434
+ (`scripts/**`, `**/examples/**`, test files, `packages/cli/src/**`,
435
+ `packages/create-plugin/src/**`) — this changeset covers the published
436
+ packages whose call sites changed:
437
+
438
+ - `@object-ui/app-shell`: `ObjectDataPage`'s dropped-URL-filter message is a
439
+ real diagnostic (data silently discarded), so it moves from `console.debug`
440
+ to `console.warn` to match the house convention.
441
+ - `@object-ui/plugin-detail`: `DetailView`'s Web Share API failure now reports
442
+ via `console.error` (it is a real failure, not debug noise); a redundant
443
+ "Link copied to clipboard" success log is removed.
444
+ - `@object-ui/fields`: `MasterDetailField`'s `handleView` stub no longer logs
445
+ the item it does nothing with.
446
+ - `@object-ui/runner`: `App`'s loader-selection debug prints, `LayoutRenderer`'s
447
+ unused click-handler stub log, and `MockDataSource`'s per-call narration
448
+ (`find`/`create`/`getObjectSchema`) are removed — none diagnosed a problem,
449
+ they only echoed the happy path.
450
+ - `object-ui` (VS Code extension): the "extension is now active!" activation
451
+ log is removed.
452
+
453
+ No behavior changes beyond console output. `@object-ui/core` and
454
+ `@object-ui/data-objectstack` also touch `no-console`-adjacent lines
455
+ (`debugLog`/`debugTime`/`debugTimeEnd`, `createQuietHttpLogger`) but only to
456
+ add `eslint-disable-next-line` documentation — those ARE the repo's
457
+ deliberate debug/logger infrastructure, not leaked residue, so their own
458
+ changeset carries empty frontmatter.
459
+ - e7747f1: fix(fields): retire the `owner` field-type alias with a loud tombstone
460
+
461
+ `owner` was a synonym for `user` with zero behavioral delta — both resolved to
462
+ the same `UserField` widget — and it is not a member of `@objectstack/spec`'s
463
+ closed `FieldType`, so no object schema could ever declare it. It was reachable
464
+ only through hand-written SDUI, and the three code faces that read it had
465
+ already drifted apart on the word: the form's data-source rule excluded it,
466
+ while plugin-grid's bulk-action dialog and app-shell's `paramToField` included
467
+ it.
468
+
469
+ The retired spelling now fails **loudly**. Deleting the alias on its own would
470
+ have been absorbed by two silent tails (`mapFieldTypeToFormType`'s
471
+ `|| 'field:text'` and `resolveFormWidgetType`'s `: 'text'`), each handing back a
472
+ working plain text input with no check turning red — so anyone who had written
473
+ `type: 'owner'`, including an AI author copying it out of a doc, would have
474
+ shipped a text box believing they shipped a person picker. Instead:
475
+
476
+ - `type: 'owner'` and `widget: 'field:owner'` both resolve to a registered
477
+ tombstone widget that renders a visible refusal naming the migration;
478
+ - the same prescription is written to the console once per spelling;
479
+ - the read/cell path degrades to the text cell deliberately and says so.
480
+
481
+ Migration: write the record-owner field as `{ type: 'user', name: 'owner' }` —
482
+ the field NAME carries the ownership meaning, the type carries the widget.
483
+ `UserField` and `UserCellRenderer` are unchanged; only the synonym is gone.
484
+
485
+ Also corrects the `dataSource` TSDoc in `@object-ui/fields`, which listed `grid`
486
+ among the widgets the form renderer wires a DataSource to. `GridField` never
487
+ read `dataSource` and no data-source table ever contained the key.
488
+ - ac2f332: Retired field-type spellings can no longer reach an inline editor by delegation — the grid's inline cell editor stops offering a working person picker for `owner`.
489
+
490
+ objectui#4814 retired the `owner` field type with a loud tombstone, and the
491
+ record form has answered `type: 'owner'` with a visible refusal ever since.
492
+ `FieldEditWidget` did not participate, and the reason it was missed is that no
493
+ routing table had to list the spelling for it to stay alive:
494
+ `hasFieldEditWidget` answers `resolveInlineEditType(type) in EDIT_WIDGETS`, and
495
+ `resolveInlineEditType` returns a type **unchanged** when it is already a key of
496
+ `EDIT_WIDGETS`. So while `owner: UserField` sat in that map, the alias table —
497
+ where the retirement lives — was never consulted, and the retirement never
498
+ applied. Measured: `hasFieldEditWidget('owner') === true`, and a direct
499
+ `FieldEditWidget` call rendered a working person picker. Every host built on
500
+ this seam, the data grid's inline cell editor being the user-facing one, still
501
+ let a person be picked and saved for a field the record form refuses.
502
+
503
+ The gate now consults the retirement table itself, so this closes the **class**
504
+ rather than the spelling — the next retirement covers this seam the day it lands
505
+ in `RETIRED_FIELD_TYPES`, with no edit here:
506
+
507
+ - `hasFieldEditWidget()` answers `false` for any retired spelling, ahead of
508
+ every table lookup (so it holds even if a retired spelling is still a key of
509
+ the widget map);
510
+ - `isInlineExcludedFieldType()` answers `true` for any retired spelling, which
511
+ is what routes a host to its read-only path;
512
+ - `FieldEditWidget` itself renders no control for a retired spelling and writes
513
+ the once-per-spelling console prescription naming the migration;
514
+ - `owner` is dropped from `EDIT_WIDGETS` and from the cosmetic
515
+ `COMPACT_EDIT_TYPES` sizing set.
516
+
517
+ The second bullet is load-bearing and not cosmetic. `ObjectGrid` marks a column
518
+ `editable: false` exactly when `isFieldInlineEditable` is false, and that
519
+ predicate consults `isInlineExcludedFieldType`. Without it, closing the first
520
+ gate alone would have left the column editable, handed the cell editor a `null`
521
+ widget, and let DataTable fall back to its built-in **plain text input** — which
522
+ for a stored person reference displays a coerced value and writes a bare string
523
+ straight back over it. Measured before/after on the grid consumer: a stored
524
+ `owner` field went from an editable cell rendering the person picker to a
525
+ read-only cell (`isFieldInlineEditable('owner')` `true` → `false`), which is the
526
+ same disposition `password`, `formula` and `composite` already get, for the same
527
+ reason. The read path is unchanged from #4814: the text cell plus the console
528
+ prescription.
529
+
530
+ `user` is untouched — it keeps the person picker, its compact sizing and its
531
+ expanded-object value resolution. The record-owner idiom survives verbatim as
532
+ `{ type: 'user', name: 'owner' }`: the field NAME carries the ownership meaning,
533
+ the type carries the widget.
534
+ - a777058: Editable `markdown` / `html` / `richtext` fields now carry the host's `id` and `aria-describedby` on the editor.
535
+
536
+ `RichTextField` — the one widget all three registry keys resolve to — read only
537
+ `className` and `disabled` off its props and had no `toDomProps` anywhere in the
538
+ file, so everything `<FormControl>`'s Radix `Slot` hands a field widget landed on
539
+ no element at all. Measured on a real form with a field carrying a description:
540
+
541
+ ```
542
+ markdown editable descEl=SET consumers=0 hostIdEl=NONE for=DANGLING
543
+ text editable descEl=SET consumers=1 hostIdEl=input for=RESOLVES
544
+ textarea editable descEl=SET consumers=1 hostIdEl=textarea for=RESOLVES
545
+ ```
546
+
547
+ Two user-visible consequences: the form's visible label pointed at an id nothing
548
+ carried, so clicking it did not reach the editor and the control had no
549
+ accessible name; and the `<FormDescription>` the form rendered below the field
550
+ had zero consumers, so a screen reader never announced the help text or — on a
551
+ failed submit — the error message id that rides in the same `aria-describedby`.
552
+
553
+ The fix is objectui#3318's standing recipe: `toDomProps(props)` spread onto the
554
+ REAL focusable control, which here is the `<Textarea>` inside the shared
555
+ `RichTextEditorSurface`. The pass-through is given to the inline surface only —
556
+ the fullscreen dialog's copy of the same surface must not carry a duplicate of
557
+ the host id, and the description ids it names sit outside the modal Radix
558
+ `aria-hidden`s. `aria-invalid` still comes from the widget's own `error` read,
559
+ kept after the spread so objectui#3318's PASS entry for this widget is unchanged.
560
+
561
+ The readonly branch is untouched: the same reading there belongs to
562
+ objectui#4788, whose mechanism is still open.
563
+ - 75444e3: The dependency-gate hint now enumerates its controlling fields with the locale's
564
+ own list separator, and reads identically whichever caller produced it.
565
+
566
+ `lookup.selectFirst` and `fields.options.selectFirst` are deliberately one
567
+ wording, so a field gated on two or more parents says the same thing whether the
568
+ lookup widget or the form renderer rendered it. The sentence was shared but its
569
+ `{{fields}}` slot was not: each call site joined the controlling-field names
570
+ with its own hardcoded separator, and not even the same one — `', '` in
571
+ `LookupField`, `' / '` in the form renderer's `gatedHint` and in
572
+ `OptionsEmptyState`. A field gated on Account and Lead Source read
573
+ `Select Account, Lead Source first` from one side and
574
+ `Select Account / Lead Source first` from the other.
575
+
576
+ A list separator is a property of the locale rather than of the code, so both
577
+ spellings were also wrong for the script under zh/ja (which enumerate with
578
+ U+3001) and under ar (U+060C). All three call sites now read
579
+ `validation.formInvalidJoiner` — the key already shipped in all ten packs for
580
+ the invalid-submit toast's field list, which is the same class of truncated-name
581
+ list. One key, every caller: a second, gate-specific key would have recreated
582
+ the divergence the shared sentence exists to prevent.
583
+
584
+ No locale pack changes, and no change to what a provider-less render produces in
585
+ English: the `@object-ui/fields` defaults table declares the joiner as `', '`,
586
+ the `en` pack's value and the literal `LookupField` previously hardcoded.
587
+ - dad51e5: fix(fields): deliver the host's a11y channels to `slider` and name `signature`
588
+
589
+ `SliderField` and `SignatureField` forwarded nothing a form host handed them —
590
+ neither spread `toDomProps(props)` at all — so `<FormControl>`'s whole payload
591
+ landed on nothing. Measured on a real form, one required field per row, freshly
592
+ failed validation:
593
+
594
+ ```
595
+ slider ariaInvalidTrue=[] labelFor=…-form-item -> DANGLING descConsumers=0 ids=[]
596
+ signature ariaInvalidTrue=[] labelFor=…-form-item -> DANGLING descConsumers=0 ids=[]
597
+ text ariaInvalidTrue=[input] labelFor -> input descConsumers=1
598
+ ```
599
+
600
+ `ids=[]` is the tell: no element in either row carried an id at all, so the
601
+ visible label pointed `for` at nothing, the rendered help text had zero
602
+ consumers, and a failed slider announced no error state.
603
+
604
+ **`slider`** now delivers all three. Its focusable control is Radix's
605
+ `span[role="slider"]` thumb, which the synced `ui/slider.tsx` renders internally
606
+ and does not export, so the primitive grew a declared `thumbProps` — routed
607
+ through a new `lib/slider-thumb` and applied to the no-touch file as a declared
608
+ sync patch, so it survives regeneration. The split of which keys stay on Root
609
+ (`name`, `disabled`) is the one the `select` fix already settled.
610
+
611
+ **`signature`** gets the name and the description on a `role="group"` container.
612
+ Its control state deliberately does not follow: the drawing surface is a
613
+ `<canvas>` with no keyboard path, and its only other element is disabled while
614
+ the pad is empty, so there is no element a control state could be read from.
615
+
616
+ Both are now declared `labelling: 'group'` — a `<span>` and a `<canvas>` are not
617
+ labelable elements, so a host `for` could only dangle at them.
618
+ - Updated dependencies [88085e3]
619
+ - Updated dependencies [69251bf]
620
+ - Updated dependencies [57e668f]
621
+ - Updated dependencies [516663d]
622
+ - Updated dependencies [41ac1b7]
623
+ - Updated dependencies [1eaf0a1]
624
+ - Updated dependencies [460c4d0]
625
+ - Updated dependencies [0ae27f7]
626
+ - Updated dependencies [2533ec5]
627
+ - Updated dependencies [78c0f9a]
628
+ - Updated dependencies [bbe8b86]
629
+ - Updated dependencies [8477be5]
630
+ - Updated dependencies [279fb13]
631
+ - Updated dependencies [2e82ab2]
632
+ - Updated dependencies [ad07b65]
633
+ - Updated dependencies [41f498b]
634
+ - Updated dependencies [ef0d150]
635
+ - Updated dependencies [f34226e]
636
+ - Updated dependencies [564b605]
637
+ - Updated dependencies [e1d4251]
638
+ - Updated dependencies [40d3a33]
639
+ - Updated dependencies [8b9dc62]
640
+ - Updated dependencies [1184192]
641
+ - Updated dependencies [a2a9747]
642
+ - Updated dependencies [a1609a6]
643
+ - Updated dependencies [53f23bc]
644
+ - Updated dependencies [c4533dc]
645
+ - Updated dependencies [be60815]
646
+ - Updated dependencies [37f6844]
647
+ - Updated dependencies [93de4f6]
648
+ - Updated dependencies [2b50261]
649
+ - Updated dependencies [384f30d]
650
+ - Updated dependencies [ac600e5]
651
+ - Updated dependencies [97fba31]
652
+ - Updated dependencies [232f61a]
653
+ - Updated dependencies [d374caf]
654
+ - Updated dependencies [5673576]
655
+ - Updated dependencies [c1ef923]
656
+ - Updated dependencies [911ceaa]
657
+ - Updated dependencies [98eab36]
658
+ - Updated dependencies [af5e292]
659
+ - Updated dependencies [3fbbea1]
660
+ - Updated dependencies [5458414]
661
+ - Updated dependencies [7f96b10]
662
+ - Updated dependencies [167ec42]
663
+ - Updated dependencies [616a2a5]
664
+ - Updated dependencies [0046d8f]
665
+ - Updated dependencies [f1d4748]
666
+ - Updated dependencies [bea374e]
667
+ - Updated dependencies [b1119ec]
668
+ - Updated dependencies [9f23d2b]
669
+ - Updated dependencies [578e025]
670
+ - Updated dependencies [af025ee]
671
+ - Updated dependencies [d109a4d]
672
+ - Updated dependencies [598c89a]
673
+ - Updated dependencies [4a0bd17]
674
+ - Updated dependencies [b8b9af4]
675
+ - Updated dependencies [31676be]
676
+ - Updated dependencies [8c0d52e]
677
+ - Updated dependencies [aff10e2]
678
+ - Updated dependencies [70a774b]
679
+ - Updated dependencies [9ce096f]
680
+ - Updated dependencies [e05db88]
681
+ - Updated dependencies [7458a41]
682
+ - Updated dependencies [ad13d63]
683
+ - Updated dependencies [5ffcc14]
684
+ - Updated dependencies [d971e51]
685
+ - Updated dependencies [97abb24]
686
+ - Updated dependencies [deb157a]
687
+ - Updated dependencies [9c60144]
688
+ - Updated dependencies [d2ce342]
689
+ - Updated dependencies [9695da7]
690
+ - Updated dependencies [75444e3]
691
+ - Updated dependencies [58b8346]
692
+ - Updated dependencies [2d0bd16]
693
+ - Updated dependencies [a9e17b4]
694
+ - Updated dependencies [b8ce7dc]
695
+ - Updated dependencies [dad51e5]
696
+ - Updated dependencies [1c9c342]
697
+ - Updated dependencies [787c738]
698
+ - Updated dependencies [8396656]
699
+ - Updated dependencies [dbbd38a]
700
+ - Updated dependencies [8871c14]
701
+ - Updated dependencies [93fe362]
702
+ - Updated dependencies [dfc6975]
703
+ - Updated dependencies [3cf4de0]
704
+ - Updated dependencies [c9dc811]
705
+ - Updated dependencies [144ef9b]
706
+ - Updated dependencies [138ab04]
707
+ - Updated dependencies [a0b9e91]
708
+ - Updated dependencies [99bd015]
709
+ - Updated dependencies [21e4585]
710
+ - @object-ui/types@17.6.0
711
+ - @object-ui/i18n@17.6.0
712
+ - @object-ui/react@17.6.0
713
+ - @object-ui/components@17.6.0
714
+ - @object-ui/core@17.6.0
715
+ - @object-ui/providers@17.6.0
716
+
3
717
  ## 17.5.0
4
718
 
5
719
  ### Minor Changes