@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
@@ -1,169 +0,0 @@
1
- import React, { useId, useEffect } from 'react';
2
- import { Checkbox, Label, EmptyValue, Badge } from '@object-ui/components';
3
- import type { OptionLike } from '@object-ui/core';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { toHostGroupProps } from './toHostGroupProps';
7
- import { OptionsEmptyState } from './OptionsEmptyState';
8
- import { useCascadingOptions } from './useCascadingOptions';
9
-
10
- type Option = OptionLike;
11
-
12
- /**
13
- * CheckboxesField - select zero or more values from a fixed option set, rendered
14
- * as a list of checkboxes. The stored value is a string[]. Used for the
15
- * `checkboxes` field type.
16
- *
17
- * Options support the same per-option `visibleWhen` cascading + `dependsOn`
18
- * gating as `MultiSelectField` (ADR-0058 / #2715), resolved through the shared
19
- * {@link useCascadingOptions} hook: the offered boxes narrow against the live
20
- * form record + `current_user`, the control is gated behind a "select the parent
21
- * first" hint while a dependency is empty, and selections no longer offered
22
- * (parent changed / predicate flipped) are pruned from the array.
23
- */
24
- export function CheckboxesField({
25
- value,
26
- onChange,
27
- field,
28
- readonly,
29
- className,
30
- dependentValues,
31
- dependsOn: dependsOnProp,
32
- emptyHint,
33
- dataSource: _dataSource,
34
- error,
35
- ...props
36
- }: FieldWidgetComponentProps<string[]>) {
37
- const config = field as any;
38
- const rawOptions: Option[] = config?.options || [];
39
- const selected: string[] = Array.isArray(value) ? value : value == null ? [] : [value as unknown as string];
40
- const groupId = useId();
41
- const fieldName = props.name || config?.name || props.id || '';
42
-
43
- const dependsOn = config?.dependsOn ?? dependsOnProp;
44
- const { options, gated, dependsOnFields } = useCascadingOptions<Option>(
45
- rawOptions,
46
- dependsOn,
47
- dependentValues,
48
- );
49
-
50
- // Cascade clear: prune selected values the offered set no longer includes
51
- // (parent changed / predicate flipped), keeping the ones still valid — the
52
- // per-element clear the multi-value case needs (cf. the scalar select/radio).
53
- useEffect(() => {
54
- if (readonly) return;
55
- // Never configured → nothing to prune against; see `MultiSelectField`'s
56
- // copy of this guard for the measured failure (objectui#4220).
57
- if (rawOptions.length === 0) return;
58
- // Gated → the authored list is withheld until the `dependsOn` parent is
59
- // chosen, so the empty offered set is missing information, not a verdict on
60
- // the stored value; clearing here fired on mount (objectui#4247). Same
61
- // reasoning, at length, in `MultiSelectField`.
62
- if (gated) return;
63
- if (selected.length === 0) return;
64
- const stillOffered = selected.filter((v) => options.some((o) => o.value === v));
65
- if (stillOffered.length !== selected.length) onChange(stillOffered);
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
- }, [options, gated]);
68
-
69
- // The host's group label has to be consumed on EVERY surface this widget can
70
- // render, not only the editable one (objectui#3990): both branches below
71
- // return before the editable container's `groupDomProps` spread, which is how
72
- // a field-level `readonly: true` and a zero-option list ended up with a
73
- // published label id and no consumer at all. See `toHostGroupProps`.
74
- const hostGroupProps = toHostGroupProps(props);
75
-
76
- if (readonly) {
77
- // The readonly display of a checkbox set is the set of CHECKED labels — a
78
- // set of values, so the same `group` answer as the editable list.
79
- // `EmptyValue` is that surface with nothing in it; its own `aria-label`
80
- // ("No value") is outranked by `aria-labelledby` per accname, and on the
81
- // `generic` role that span carries it was never exposed anyway.
82
- if (selected.length === 0) return <EmptyValue {...hostGroupProps} />;
83
- return (
84
- <div {...hostGroupProps} className="flex flex-wrap gap-1">
85
- {selected.map((v) => {
86
- // Label from the raw set so a stored value hidden by `visibleWhen`
87
- // still renders its label rather than a bare id.
88
- const opt = rawOptions.find((o) => o.value === v);
89
- return <Badge key={v} variant="outline">{opt?.label || v}</Badge>;
90
- })}
91
- </div>
92
- );
93
- }
94
-
95
- // No offered options is unfillable — surface a legible state instead of an
96
- // empty checkbox list: the host's `emptyHint` when it computed one, else this
97
- // widget's own translated copy. Shared with the select / multiselect / radio
98
- // so the four cannot drift again (objectui#3231).
99
- if (options.length === 0) {
100
- return (
101
- <OptionsEmptyState
102
- emptyHint={emptyHint}
103
- gated={gated}
104
- dependsOnFields={dependsOnFields}
105
- testId={fieldName ? `checkboxes-empty-${fieldName}` : undefined}
106
- className="min-h-9"
107
- // This box IS the field in this state, so it is what the host label
108
- // names (objectui#3990).
109
- hostGroupProps={hostGroupProps}
110
- />
111
- );
112
- }
113
-
114
- const toggle = (v: string, checked: boolean) => {
115
- const next = checked ? [...selected.filter((x) => x !== v), v] : selected.filter((x) => x !== v);
116
- onChange(next);
117
- };
118
-
119
- // DOM pass-through (objectui#3318): the container carries the form
120
- // renderer's id / aria-describedby, but NOT its `aria-invalid` — a plain
121
- // wrapper div is not where assistive tech reads the invalid state. That
122
- // state goes onto every focusable checkbox below (`checkbox` is an
123
- // aria-invalid-supporting role), computed from the published `error` slot
124
- // (#3222). The per-item ids below stay authoritative for their labels.
125
- // `name` is withheld too: it is only DOM-legal on form controls, and on
126
- // this div it is exactly the leak #3291 sweeps for.
127
- const {
128
- 'aria-invalid': _hostAriaInvalid,
129
- name: _domName,
130
- ...groupDomProps
131
- } = toDomProps(props);
132
- // When the host associated its visible label with this container by IDREF
133
- // (`aria-labelledby`, objectui#3961) the container IS the labelled group, so it
134
- // answers with the matching role — without one, the name sits on a generic
135
- // `div` and contributes nothing: `label for` pointing here was already inert
136
- // (`HTMLLabelElement.control` is null for a div), which is the whole defect.
137
- // Absent (standalone: the inline grid editor, a bare SDUI node) nothing is
138
- // emitted and the markup is unchanged.
139
- const isLabelledGroup = groupDomProps['aria-labelledby'] != null;
140
-
141
- return (
142
- <div
143
- {...groupDomProps}
144
- role={isLabelledGroup ? 'group' : undefined}
145
- className={className}
146
- data-testid={fieldName ? `checkboxes-${fieldName}` : undefined}
147
- >
148
- {options.map((opt) => {
149
- // Multi-value fields store string arrays — stringify the (possibly
150
- // numeric, #3090-widened) authored value at this boundary.
151
- const value = String(opt.value);
152
- const id = `${groupId}-${value}`;
153
- return (
154
- <div key={value} className="flex items-center space-x-2 py-0.5">
155
- <Checkbox
156
- id={id}
157
- checked={selected.includes(value)}
158
- onCheckedChange={(checked) => toggle(value, !!checked)}
159
- disabled={props.disabled}
160
- aria-invalid={!!error}
161
- data-testid={`checkboxes-option-${value}`}
162
- />
163
- <Label htmlFor={id} className="font-normal">{opt.label}</Label>
164
- </div>
165
- );
166
- })}
167
- </div>
168
- );
169
- }
@@ -1,41 +0,0 @@
1
- import React from 'react';
2
- import { Textarea, cn, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * Code field widget - provides a code editor with syntax highlighting
8
- * Uses a simple textarea with monospace font
9
- * For advanced code editing, use the @object-ui/plugin-editor component
10
- */
11
- export function CodeField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
12
- const config = field;
13
- // Get code-specific configuration from field metadata
14
- const language = (config as any)?.language ?? 'javascript';
15
-
16
- if (readonly) {
17
- return (
18
- <pre className={cn("text-sm bg-muted p-2 rounded overflow-x-auto border", props.className)}>
19
- <code>{value || <EmptyValue />}</code>
20
- </pre>
21
- );
22
- }
23
-
24
- return (
25
- <Textarea
26
- // DOM pass-through onto the real focusable control (objectui#3318).
27
- {...toDomProps(props)}
28
- value={value || ''}
29
- onChange={(e) => onChange(e.target.value)}
30
- placeholder={config?.placeholder || `// Write ${language} code here...`}
31
- disabled={readonly || props.disabled}
32
- className={cn("font-mono text-sm", props.className)}
33
- rows={12}
34
- spellCheck={false}
35
- // AFTER the spread so this widget's own computation wins: `error` is
36
- // the published validation slot (#3222), `!!undefined` → explicit
37
- // "false".
38
- aria-invalid={!!error}
39
- />
40
- );
41
- }
@@ -1,53 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * Color field widget - provides a color picker input
8
- * Supports hex color values (e.g., #ff0000)
9
- */
10
- export function ColorField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
11
- const colorField = field as any;
12
-
13
- if (readonly) {
14
- return (
15
- <div className="flex items-center gap-2">
16
- <div
17
- className="w-6 h-6 rounded border border-input"
18
- style={{ backgroundColor: value || '#000000' }}
19
- />
20
- <span className="text-sm">{value || <EmptyValue />}</span>
21
- </div>
22
- );
23
- }
24
-
25
- return (
26
- <div className="flex items-center gap-2">
27
- <input
28
- type="color"
29
- value={value || '#000000'}
30
- onChange={(e) => onChange(e.target.value)}
31
- disabled={readonly || props.disabled}
32
- className="w-10 h-10 rounded border border-input cursor-pointer"
33
- // Both focusable halves of this widget announce the same validation
34
- // state (objectui#3318).
35
- aria-invalid={!!error}
36
- />
37
- <Input
38
- // DOM pass-through onto the primary text control (objectui#3318): the
39
- // whitelist spread carries the form renderer's id / aria-describedby.
40
- {...toDomProps(props)}
41
- type="text"
42
- value={value || ''}
43
- onChange={(e) => onChange(e.target.value)}
44
- placeholder={colorField?.placeholder || '#000000'}
45
- disabled={readonly || props.disabled}
46
- className={props.className}
47
- pattern="^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
48
- // AFTER the spread so this widget's own computation wins (#3222).
49
- aria-invalid={!!error}
50
- />
51
- </div>
52
- );
53
- }
@@ -1,135 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
- import { useLocalization, useDisplayLocale, formatDisplayNumber } from '@object-ui/i18n';
6
- import { resolveFieldCurrency, currencyFractionDigits, currencySymbol } from '../currency';
7
-
8
- /**
9
- * Format currency value for display. When `currency` is undefined the value
10
- * is rendered as a plain number with thousands separators (no symbol),
11
- * because silently assuming USD is misleading for non-USD businesses.
12
- *
13
- * `precision` is a display width the caller resolved — either the field's
14
- * authored `precision` or, when it declared none, the currency's own ISO 4217
15
- * minor-unit count (see the derivation at the call site, objectui#4361).
16
- */
17
- function formatAmount(
18
- value: number,
19
- currency: string | undefined,
20
- precision: number,
21
- locale?: string,
22
- ): string {
23
- if (currency) {
24
- try {
25
- return formatDisplayNumber(value, {
26
- locale,
27
- currency,
28
- minimumFractionDigits: precision,
29
- maximumFractionDigits: precision,
30
- });
31
- } catch {
32
- return `${currency} ${value.toFixed(precision)}`;
33
- }
34
- }
35
- try {
36
- // No `scale` passed: this is a currency widget whose `precision` is a
37
- // display width, so a `precision: 0` amount keeps its separators rather
38
- // than being read as an ordinal (objectui#4033).
39
- return formatDisplayNumber(value, {
40
- locale,
41
- minimumFractionDigits: precision,
42
- maximumFractionDigits: precision,
43
- });
44
- } catch {
45
- return value.toFixed(precision);
46
- }
47
- }
48
-
49
- export function CurrencyField({ value, onChange, field, readonly, error, className, ...props }: FieldWidgetComponentProps<number>) {
50
- const currencyField = field as any;
51
- // Shared precedence: field currency → currencyConfig → tenant default (ADR-0053).
52
- const { currency: tenantCurrency } = useLocalization();
53
- const locale = useDisplayLocale();
54
- const currency: string | undefined = resolveFieldCurrency(currencyField, tenantCurrency);
55
- // An AUTHORED `precision` wins: it is authored metadata, and this repo's
56
- // convention is that authored metadata keeps priority. Whether a declared
57
- // `precision` that contradicts the currency's ISO 4217 digits (say
58
- // `precision: 2` on a JPY field) should be REJECTED at publish time is a
59
- // contract question, so it is filed upstream in `@objectstack/spec` rather
60
- // than answered here by overriding the author (objectui#4361).
61
- //
62
- // An ABSENT `precision` derives from the currency instead of defaulting to a
63
- // literal 2, which rendered `¥1,234.50` for a currency with no minor unit.
64
- // "Absent" is genuinely distinguishable from "authored 2" here — MEASURED,
65
- // not assumed: `CurrencyFieldMetadata.precision` is optional in
66
- // `@object-ui/types` and `z.ZodOptional<z.ZodNumber>` (no `.default()`) in
67
- // `@objectstack/spec`, so a parsed field carries no materialized 2. The only
68
- // `.default(2)` on the currency surface is `CurrencyConfigSchema.precision`,
69
- // a different key on the `currencyConfig` block that this widget never reads.
70
- //
71
- // This is the widget's ONE precision, so the derivation also reaches the edit
72
- // affordances below. Deliberate: leaving `step`/blur-rounding at 2 would give
73
- // a JPY field that displays whole yen while offering a 0.01 spinner step and
74
- // rounding typed input to 1234.56 yen.
75
- const precision =
76
- currencyField?.precision ?? (currency ? currencyFractionDigits(currency) : 2);
77
-
78
- if (readonly) {
79
- if (value == null) return <EmptyValue />;
80
- return (
81
- <span className="text-sm font-medium tabular-nums">
82
- {formatAmount(Number(value), currency, precision, locale)}
83
- </span>
84
- );
85
- }
86
-
87
- // Parse and format on blur to ensure valid currency format
88
- const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
89
- const val = parseFloat(e.target.value);
90
- if (!isNaN(val)) {
91
- onChange(parseFloat(val.toFixed(precision)));
92
- }
93
- };
94
-
95
- // ONE channel for the symbol (objectui#4414). This used to be a hand-written
96
- // `currency === 'USD' ? '$' : currency` ternary, with a dead one-entry
97
- // `CURRENCY_SYMBOLS` map sitting two lines above it restating the same fact —
98
- // a table that looked like the place to add a currency but that nothing read.
99
- // Both were hand copies of knowledge `Intl` already carries, and both are
100
- // gone: `currencySymbol` reads the `currency` part of the SAME format the
101
- // readonly branch above renders amounts with, so the two modes of this widget
102
- // can no longer disagree (they did: `JPY` in the adornment, `¥1,235` in the
103
- // readonly span). USD at the display-locale default is `$` either way.
104
- const symbol = currency ? currencySymbol(currency, locale) : '';
105
-
106
- return (
107
- <div className="relative">
108
- {symbol && (
109
- <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">
110
- {symbol}
111
- </span>
112
- )}
113
- <Input
114
- {...toDomProps(props)}
115
- type="number"
116
- value={value ?? ''}
117
- onChange={(e) => {
118
- const val = e.target.value === '' ? null : parseFloat(e.target.value);
119
- onChange(val as any);
120
- }}
121
- onBlur={handleBlur}
122
- placeholder={currencyField?.placeholder || '0.00'}
123
- disabled={readonly || props.disabled}
124
- className={`${symbol ? 'pl-8' : ''} ${className || ''}`}
125
- // Surface the field's declared range (e.g. `min: 0` on a budget) so the
126
- // browser's spinner/keyboard affordances respect it (objectui#2572);
127
- // server-side validation still owns enforcement.
128
- min={typeof currencyField?.min === 'number' ? currencyField.min : undefined}
129
- max={typeof currencyField?.max === 'number' ? currencyField.max : undefined}
130
- step={Math.pow(10, -precision).toFixed(precision)}
131
- aria-invalid={!!error}
132
- />
133
- </div>
134
- );
135
- }
@@ -1,40 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { useDisplayLocale } from '@object-ui/i18n';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { openNativePicker } from './openNativePicker';
7
- import { toDateInputValue } from './nativeDateValue';
8
-
9
- /**
10
- * DateField - Date picker input widget
11
- * Uses native date input and displays locale-formatted date in readonly mode
12
- */
13
- export function DateField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
14
- // Before the readonly early return: the hook count must not depend on a prop
15
- // (objectui#4468). A bare `toLocaleDateString()` reads the MACHINE's locale,
16
- // which is how a Chinese form ended up with an `8/11/2026` value in it.
17
- const locale = useDisplayLocale();
18
- if (readonly) {
19
- return value ? <span className="text-sm">{new Date(value).toLocaleDateString(locale)}</span> : <EmptyValue />;
20
- }
21
-
22
- const domProps = toDomProps(props);
23
-
24
- return (
25
- <Input
26
- {...domProps}
27
- type="date"
28
- // An API that hands back `2026-06-17T00:00:00.000Z` for a `date` field
29
- // would leave this control empty too (objectui#3127). The written-back
30
- // shape is unchanged: the control's own plain `YYYY-MM-DD`.
31
- value={toDateInputValue(value)}
32
- onChange={(e) => onChange(e.target.value)}
33
- onClick={(e) => {
34
- openNativePicker(e.currentTarget);
35
- domProps.onClick?.(e);
36
- }}
37
- disabled={readonly || domProps.disabled}
38
- />
39
- );
40
- }
@@ -1,46 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { useDisplayLocale } from '@object-ui/i18n';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { openNativePicker } from './openNativePicker';
7
- import { toDateTimeInputValue, fromDateTimeInputValue } from './nativeDateValue';
8
-
9
- /**
10
- * DateTimeField - Combined date and time picker widget
11
- * Displays both date and time in locale format when readonly
12
- */
13
- export function DateTimeField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
14
- // Before the readonly early return — the hook count must not depend on a
15
- // prop. See DateField for why the bare `toLocale*` calls were wrong
16
- // (objectui#4468).
17
- const locale = useDisplayLocale();
18
- if (readonly) {
19
- if (!value) return <EmptyValue />;
20
- const date = new Date(value);
21
- return (
22
- <span className="text-sm">
23
- {date.toLocaleDateString(locale)} {date.toLocaleTimeString(locale)}
24
- </span>
25
- );
26
- }
27
-
28
- const domProps = toDomProps(props);
29
-
30
- return (
31
- <Input
32
- {...domProps}
33
- type="datetime-local"
34
- // The record's value is ISO-8601 (`…T14:30:00.000Z`), which this control
35
- // rejects outright — it renders empty and the user reads that as a lost
36
- // value (objectui#3127). Convert in, and back out on the same basis.
37
- value={toDateTimeInputValue(value)}
38
- onChange={(e) => onChange(fromDateTimeInputValue(e.target.value))}
39
- onClick={(e) => {
40
- openNativePicker(e.currentTarget);
41
- domProps.onClick?.(e);
42
- }}
43
- disabled={readonly || domProps.disabled}
44
- />
45
- );
46
- }
@@ -1,37 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * EmailField - Email address input with mailto link in readonly mode
8
- * Renders as a clickable mailto link when readonly, standard email input otherwise
9
- */
10
- export function EmailField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
11
- const config = field;
12
- if (readonly) {
13
- if (!value) return <EmptyValue />;
14
- return (
15
- <a
16
- href={`mailto:${value}`}
17
- className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
18
- >
19
- {value}
20
- </a>
21
- );
22
- }
23
-
24
- const domProps = toDomProps(props);
25
-
26
- return (
27
- <Input
28
- {...domProps}
29
- type="email"
30
- value={value || ''}
31
- onChange={(e) => onChange(e.target.value)}
32
- placeholder={config?.placeholder || 'email@example.com'}
33
- disabled={readonly || domProps.disabled}
34
- aria-invalid={!!error}
35
- />
36
- );
37
- }