@object-ui/fields 17.3.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 (265) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +1 -12
  3. package/dist/FieldEditWidget.d.ts +23 -3
  4. package/dist/currency.d.ts +46 -0
  5. package/dist/field-type-alias.d.ts +35 -1
  6. package/dist/index.cjs +4 -4
  7. package/dist/index.css +651 -0
  8. package/dist/index.d.ts +474 -85
  9. package/dist/index.js +5418 -5023
  10. package/dist/widgets/AddressField.d.ts +4 -12
  11. package/dist/widgets/AutoNumberField.d.ts +1 -1
  12. package/dist/widgets/AvatarField.d.ts +1 -1
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  15. package/dist/widgets/CheckboxesField.d.ts +1 -1
  16. package/dist/widgets/CodeField.d.ts +1 -1
  17. package/dist/widgets/ColorField.d.ts +1 -1
  18. package/dist/widgets/CurrencyField.d.ts +1 -1
  19. package/dist/widgets/DateField.d.ts +1 -1
  20. package/dist/widgets/DateTimeField.d.ts +1 -1
  21. package/dist/widgets/EmailField.d.ts +1 -1
  22. package/dist/widgets/FileField.d.ts +18 -2
  23. package/dist/widgets/FilterConditionField.d.ts +28 -1
  24. package/dist/widgets/FormulaField.d.ts +1 -1
  25. package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
  26. package/dist/widgets/GeolocationField.d.ts +1 -1
  27. package/dist/widgets/GridField.d.ts +68 -6
  28. package/dist/widgets/ImageField.d.ts +1 -1
  29. package/dist/widgets/LocationField.d.ts +1 -1
  30. package/dist/widgets/LookupField.d.ts +1 -1
  31. package/dist/widgets/MasterDetailField.d.ts +1 -1
  32. package/dist/widgets/MultiSelectField.d.ts +1 -1
  33. package/dist/widgets/NumberField.d.ts +1 -1
  34. package/dist/widgets/ObjectField.d.ts +1 -1
  35. package/dist/widgets/ObjectRefField.d.ts +1 -1
  36. package/dist/widgets/OptionsEmptyState.d.ts +21 -1
  37. package/dist/widgets/PasswordField.d.ts +1 -1
  38. package/dist/widgets/PercentField.d.ts +1 -1
  39. package/dist/widgets/PhoneField.d.ts +1 -1
  40. package/dist/widgets/QRCodeField.d.ts +1 -1
  41. package/dist/widgets/RadioField.d.ts +1 -1
  42. package/dist/widgets/RatingField.d.ts +1 -1
  43. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  44. package/dist/widgets/RecordPickerDialog.d.ts +34 -10
  45. package/dist/widgets/RichTextField.d.ts +37 -1
  46. package/dist/widgets/SelectField.d.ts +26 -6
  47. package/dist/widgets/SignatureField.d.ts +32 -2
  48. package/dist/widgets/SliderField.d.ts +37 -2
  49. package/dist/widgets/SummaryField.d.ts +1 -1
  50. package/dist/widgets/TagsField.d.ts +1 -1
  51. package/dist/widgets/TextAreaField.d.ts +6 -2
  52. package/dist/widgets/TextField.d.ts +1 -1
  53. package/dist/widgets/TimeField.d.ts +1 -1
  54. package/dist/widgets/UrlField.d.ts +1 -1
  55. package/dist/widgets/UserField.d.ts +1 -1
  56. package/dist/widgets/VectorField.d.ts +1 -1
  57. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  58. package/dist/widgets/address-format.d.ts +88 -0
  59. package/dist/widgets/file-size-guard.d.ts +41 -0
  60. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  61. package/dist/widgets/nativeDateValue.d.ts +37 -0
  62. package/dist/widgets/richTextDisplay.d.ts +80 -0
  63. package/dist/widgets/toDomProps.d.ts +30 -10
  64. package/dist/widgets/toHostGroupProps.d.ts +189 -0
  65. package/dist/widgets/types.d.ts +8 -3
  66. package/dist/widgets/useRecordQuery.d.ts +7 -1
  67. package/package.json +17 -14
  68. package/dist/FieldEditWidget.test.d.ts +0 -8
  69. package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
  70. package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
  71. package/dist/__tests__/cell-truncation.test.d.ts +0 -0
  72. package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
  73. package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
  74. package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
  75. package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
  76. package/dist/__tests__/registry-collision.test.d.ts +0 -0
  77. package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
  78. package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
  79. package/dist/__tests__/validation-feedback.test.d.ts +0 -0
  80. package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
  81. package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
  82. package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
  83. package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
  84. package/dist/coerce-safe-value.test.d.ts +0 -1
  85. package/dist/complex-widgets.test.d.ts +0 -1
  86. package/dist/datetime-widgets.test.d.ts +0 -1
  87. package/dist/field-type-coverage.test.d.ts +0 -1
  88. package/dist/standard-widgets.test.d.ts +0 -0
  89. package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
  90. package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
  91. package/dist/widgets/CharacterCount.d.ts +0 -56
  92. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  93. package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
  94. package/dist/widgets/GridField.test.d.ts +0 -1
  95. package/dist/widgets/ImageLightbox.test.d.ts +0 -1
  96. package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
  97. package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
  98. package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
  99. package/dist/widgets/LookupField.idField.test.d.ts +0 -1
  100. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  101. package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
  102. package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
  103. package/dist/widgets/PeoplePicker.test.d.ts +0 -8
  104. package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
  105. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  106. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  107. package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
  108. package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
  109. package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
  110. package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
  111. package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
  112. package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
  113. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
  114. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
  115. package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
  116. package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
  117. package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
  118. package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
  119. package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
  120. package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
  121. package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
  122. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
  123. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
  124. package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
  125. package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
  126. package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
  127. package/dist/widgets/file-value.test.d.ts +0 -1
  128. package/dist/widgets/personDisplay.test.d.ts +0 -8
  129. package/dist/widgets/recentLookups.test.d.ts +0 -1
  130. package/dist/widgets/useRecordQuery.test.d.ts +0 -8
  131. package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
  132. package/src/FieldEditWidget.test.ts +0 -133
  133. package/src/FieldEditWidget.tsx +0 -191
  134. package/src/__tests__/DateCellRenderer.test.tsx +0 -73
  135. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  136. package/src/__tests__/cell-truncation.test.tsx +0 -96
  137. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  138. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  139. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  140. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  141. package/src/__tests__/registry-collision.test.ts +0 -91
  142. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  143. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  144. package/src/__tests__/validation-feedback.test.tsx +0 -361
  145. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  146. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  147. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
  148. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  149. package/src/coerce-safe-value.test.ts +0 -34
  150. package/src/complex-widgets.test.tsx +0 -908
  151. package/src/currency.ts +0 -14
  152. package/src/datetime-widgets.test.tsx +0 -93
  153. package/src/field-type-alias.ts +0 -100
  154. package/src/field-type-coverage.test.ts +0 -67
  155. package/src/index.tsx +0 -2503
  156. package/src/standard-widgets.test.tsx +0 -357
  157. package/src/widgets/AddressField.tsx +0 -132
  158. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
  159. package/src/widgets/AutoNumberField.tsx +0 -18
  160. package/src/widgets/AvatarField.tsx +0 -117
  161. package/src/widgets/BooleanField.tsx +0 -52
  162. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  163. package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
  164. package/src/widgets/CharacterCount.tsx +0 -263
  165. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  166. package/src/widgets/CheckboxesField.tsx +0 -136
  167. package/src/widgets/CodeField.tsx +0 -41
  168. package/src/widgets/ColorField.tsx +0 -53
  169. package/src/widgets/CurrencyField.tsx +0 -95
  170. package/src/widgets/DateField.tsx +0 -31
  171. package/src/widgets/DateTimeField.tsx +0 -37
  172. package/src/widgets/EmailField.tsx +0 -37
  173. package/src/widgets/FileField.tsx +0 -494
  174. package/src/widgets/FilterConditionField.tsx +0 -454
  175. package/src/widgets/FormulaField.tsx +0 -33
  176. package/src/widgets/FullscreenFieldEditor.tsx +0 -271
  177. package/src/widgets/GeolocationField.tsx +0 -167
  178. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  179. package/src/widgets/GridField.test.tsx +0 -406
  180. package/src/widgets/GridField.tsx +0 -966
  181. package/src/widgets/ImageCropperDialog.tsx +0 -245
  182. package/src/widgets/ImageField.tsx +0 -255
  183. package/src/widgets/ImageLightbox.test.tsx +0 -52
  184. package/src/widgets/ImageLightbox.tsx +0 -108
  185. package/src/widgets/LocationField.tsx +0 -57
  186. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  187. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  188. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  189. package/src/widgets/LookupField.idField.test.tsx +0 -61
  190. package/src/widgets/LookupField.tsx +0 -1291
  191. package/src/widgets/MarkdownContent.tsx +0 -19
  192. package/src/widgets/MasterDetailField.tsx +0 -131
  193. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  194. package/src/widgets/MultiSelectField.tsx +0 -142
  195. package/src/widgets/NumberField.tsx +0 -49
  196. package/src/widgets/ObjectField.tsx +0 -94
  197. package/src/widgets/ObjectRefField.tsx +0 -121
  198. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  199. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  200. package/src/widgets/OptionsEmptyState.tsx +0 -83
  201. package/src/widgets/PasswordField.tsx +0 -49
  202. package/src/widgets/PeoplePicker.test.tsx +0 -360
  203. package/src/widgets/PeoplePicker.tsx +0 -579
  204. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  205. package/src/widgets/PercentField.tsx +0 -95
  206. package/src/widgets/PersonRow.tsx +0 -117
  207. package/src/widgets/PhoneField.tsx +0 -37
  208. package/src/widgets/QRCodeField.tsx +0 -103
  209. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  210. package/src/widgets/RadioField.tsx +0 -111
  211. package/src/widgets/RatingField.tsx +0 -81
  212. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  213. package/src/widgets/RecipientPickerField.tsx +0 -188
  214. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  215. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  216. package/src/widgets/RecordPickerDialog.tsx +0 -1316
  217. package/src/widgets/RichTextField.tsx +0 -176
  218. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  219. package/src/widgets/SelectField.tsx +0 -162
  220. package/src/widgets/SelectionTray.tsx +0 -110
  221. package/src/widgets/SignatureField.tsx +0 -140
  222. package/src/widgets/SliderField.tsx +0 -39
  223. package/src/widgets/SummaryField.tsx +0 -31
  224. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  225. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  226. package/src/widgets/TagsField.tsx +0 -88
  227. package/src/widgets/TextAreaField.tsx +0 -213
  228. package/src/widgets/TextField.tsx +0 -45
  229. package/src/widgets/TimeField.tsx +0 -31
  230. package/src/widgets/UrlField.tsx +0 -46
  231. package/src/widgets/UserField.tsx +0 -58
  232. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  233. package/src/widgets/VectorField.tsx +0 -29
  234. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  235. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
  236. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  237. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  238. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  239. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  240. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  241. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  242. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  243. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  244. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  245. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  246. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  247. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  248. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  249. package/src/widgets/deriveLookupColumns.ts +0 -108
  250. package/src/widgets/file-value.test.ts +0 -230
  251. package/src/widgets/file-value.ts +0 -246
  252. package/src/widgets/openNativePicker.ts +0 -18
  253. package/src/widgets/personDisplay.test.ts +0 -113
  254. package/src/widgets/personDisplay.ts +0 -92
  255. package/src/widgets/recentLookups.test.ts +0 -45
  256. package/src/widgets/recentLookups.ts +0 -47
  257. package/src/widgets/toDomProps.ts +0 -175
  258. package/src/widgets/types.ts +0 -242
  259. package/src/widgets/useCascadingOptions.ts +0 -46
  260. package/src/widgets/useFieldTranslation.ts +0 -150
  261. package/src/widgets/useRecordQuery.test.ts +0 -174
  262. package/src/widgets/useRecordQuery.ts +0 -264
  263. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  264. package/src/widgets/useUploadingSignal.ts +0 -20
  265. package/src/withFieldCarrier.tsx +0 -75
@@ -1,95 +0,0 @@
1
- import React from 'react';
2
- import { Input, Slider, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * PercentField - Percentage input with configurable decimal precision
8
- * Stores values as decimals (0-1) and displays as percentages (0-100%)
9
- * Includes a slider for interactive control.
10
- */
11
- export function PercentField({ value, onChange, field, readonly, error, className, ...props }: FieldWidgetComponentProps<number>) {
12
- const percentField = field as any;
13
- const precision = percentField?.precision ?? 2;
14
-
15
- // Convention detection. A field declaring `max > 1` (e.g. `max: 100`) stores
16
- // WHOLE-NUMBER percents (0–100); otherwise values are FRACTIONS (0–1) shown
17
- // as 0–100%. This matches the read-side formatter so the edit widget agrees
18
- // with display — and, crucially, keeps the rendered <input> within its `max`
19
- // (a whole-number 50 must show "50", not "5000", or HTML5 constraint
20
- // validation marks the field `:invalid` and blocks the whole form's submit).
21
- const maxAttr = typeof percentField?.max === 'number' ? (percentField.max as number) : undefined;
22
- const whole = maxAttr != null && maxAttr > 1;
23
- const toDisplay = (v: number) => (whole ? v : v * 100);
24
- const fromDisplay = (n: number) => (whole ? n : n / 100);
25
- const sliderMax = whole ? maxAttr! : 100;
26
-
27
- if (readonly) {
28
- if (value == null) return <EmptyValue />;
29
- return (
30
- <span className="text-sm font-medium tabular-nums">
31
- {toDisplay(value).toFixed(precision)}%
32
- </span>
33
- );
34
- }
35
-
36
- // Convert between stored value and 0–100 display value
37
- const displayValue = value != null ? toDisplay(value) : '';
38
- const sliderValue = value != null ? toDisplay(value) : 0;
39
-
40
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
41
- if (e.target.value === '') {
42
- onChange(null as any);
43
- return;
44
- }
45
- const parsed = parseFloat(e.target.value);
46
- const val = isNaN(parsed) ? null : fromDisplay(parsed);
47
- onChange(val as any);
48
- };
49
-
50
- const handleSliderChange = (values: number[]) => {
51
- if (readonly || props.disabled) return;
52
- if (!Array.isArray(values) || values.length === 0) {
53
- onChange(null as any);
54
- return;
55
- }
56
- const raw = values[0];
57
- const nextValue = typeof raw === 'number' ? fromDisplay(raw) : null;
58
- onChange(nextValue as any);
59
- };
60
-
61
- // Derive slider step from precision so slider granularity matches the input
62
- const sliderStep = Math.pow(10, -precision);
63
-
64
- return (
65
- <div className="space-y-2">
66
- <div className="relative">
67
- <Input
68
- {...toDomProps(props)}
69
- type="number"
70
- value={displayValue}
71
- onChange={handleChange}
72
- placeholder={percentField?.placeholder || '0'}
73
- disabled={readonly || props.disabled}
74
- className={`pr-8 ${className || ''}`}
75
- step={Math.pow(10, -precision).toFixed(precision)}
76
- aria-invalid={!!error}
77
- />
78
- <span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">
79
- %
80
- </span>
81
- </div>
82
- <Slider
83
- value={[sliderValue]}
84
- onValueChange={handleSliderChange}
85
- min={0}
86
- max={sliderMax}
87
- step={sliderStep}
88
- disabled={readonly || props.disabled}
89
- className="w-full"
90
- aria-label="Percentage"
91
- data-testid="percent-slider"
92
- />
93
- </div>
94
- );
95
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import React, { useEffect, useRef } from 'react';
10
- import { Avatar, AvatarFallback, AvatarImage, cn } from '@object-ui/components';
11
- import { Check } from 'lucide-react';
12
- import {
13
- getPersonName,
14
- getPersonInitials,
15
- getPersonSubtitle,
16
- getPersonAvatarUrl,
17
- matchRanges,
18
- } from './personDisplay';
19
-
20
- /**
21
- * A rich, single-line candidate row for the search-first PeoplePicker:
22
- * avatar (image with initials fallback) + name + subtitle (department · email).
23
- * The subtitle is what lets search stand in for an org tree — it disambiguates
24
- * same-named people inline. The typed query is highlighted in both lines.
25
- * Selectable/toggleable; a Check marks the selected state, and `active` reflects
26
- * the keyboard cursor (scrolls itself into view).
27
- */
28
- export interface PersonRowProps {
29
- record: any;
30
- displayField?: string;
31
- subtitleFields?: string[];
32
- avatarField?: string;
33
- selected?: boolean;
34
- /** Keyboard cursor is on this row — highlight + scroll into view. */
35
- active?: boolean;
36
- /** Typed search term; matches are highlighted in name + subtitle. */
37
- highlightQuery?: string;
38
- onSelect?: (record: any) => void;
39
- className?: string;
40
- }
41
-
42
- /** Wrap literal matches of `query` in a subtle highlight. */
43
- function Highlighted({ text, query }: { text: string; query?: string }): React.ReactElement {
44
- const ranges = query ? matchRanges(text, query) : [];
45
- if (ranges.length === 0) return <>{text}</>;
46
- const out: React.ReactNode[] = [];
47
- let cursor = 0;
48
- ranges.forEach(([start, end], i) => {
49
- if (start > cursor) out.push(text.slice(cursor, start));
50
- out.push(
51
- <mark key={i} className="rounded-[2px] bg-primary/20 px-px text-inherit">
52
- {text.slice(start, end)}
53
- </mark>,
54
- );
55
- cursor = end;
56
- });
57
- if (cursor < text.length) out.push(text.slice(cursor));
58
- return <>{out}</>;
59
- }
60
-
61
- export function PersonRow({
62
- record,
63
- displayField = 'name',
64
- subtitleFields,
65
- avatarField = 'image',
66
- selected = false,
67
- active = false,
68
- highlightQuery,
69
- onSelect,
70
- className,
71
- }: PersonRowProps) {
72
- const name = getPersonName(record, displayField);
73
- const subtitle = getPersonSubtitle(record, subtitleFields);
74
- const avatarUrl = getPersonAvatarUrl(record, avatarField);
75
- const initials = getPersonInitials(name);
76
-
77
- const ref = useRef<HTMLButtonElement>(null);
78
- useEffect(() => {
79
- // Optional-call the method too — jsdom doesn't implement scrollIntoView.
80
- if (active) ref.current?.scrollIntoView?.({ block: 'nearest' });
81
- }, [active]);
82
-
83
- return (
84
- <button
85
- ref={ref}
86
- type="button"
87
- tabIndex={-1}
88
- onClick={() => onSelect?.(record)}
89
- aria-pressed={selected}
90
- data-testid="person-row"
91
- data-active={active || undefined}
92
- className={cn(
93
- 'flex w-full items-center gap-3 rounded-md px-2 py-1.5 text-left transition-colors',
94
- 'hover:bg-accent focus-visible:outline-none',
95
- selected && 'bg-accent',
96
- active && 'bg-accent ring-1 ring-inset ring-ring',
97
- className,
98
- )}
99
- >
100
- <Avatar className="size-9 shrink-0">
101
- {avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
102
- <AvatarFallback className="text-xs">{initials}</AvatarFallback>
103
- </Avatar>
104
- <div className="min-w-0 flex-1">
105
- <div className="truncate text-sm font-medium">
106
- {name ? <Highlighted text={name} query={highlightQuery} /> : '—'}
107
- </div>
108
- {subtitle && (
109
- <div className="truncate text-xs text-muted-foreground">
110
- <Highlighted text={subtitle} query={highlightQuery} />
111
- </div>
112
- )}
113
- </div>
114
- {selected && <Check className="size-4 shrink-0 text-primary" aria-hidden />}
115
- </button>
116
- );
117
- }
@@ -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
- * PhoneField - Telephone number input with tel link in readonly mode
8
- * Renders as a clickable tel link when readonly, standard phone input otherwise
9
- */
10
- export function PhoneField({ 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={`tel:${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="tel"
30
- value={value || ''}
31
- onChange={(e) => onChange(e.target.value)}
32
- placeholder={config?.placeholder || '(555) 123-4567'}
33
- disabled={readonly || domProps.disabled}
34
- aria-invalid={!!error}
35
- />
36
- );
37
- }
@@ -1,103 +0,0 @@
1
- import React from 'react';
2
- import { Input, Button, EmptyValue } from '@object-ui/components';
3
- import { QrCode, Copy } from 'lucide-react';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
-
7
- /**
8
- * QR Code field widget - generates QR codes from text
9
- * Uses a simple SVG-based QR code generator
10
- */
11
- export function QRCodeField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
12
- const [showQR, setShowQR] = React.useState(false);
13
- const config = field;
14
-
15
- // Simple QR code generation using an external library would be ideal
16
- // For now, we'll use a service API approach or placeholder
17
- const getQRCodeUrl = (text: string): string => {
18
- // Using a simple QR code API service (you might want to replace this with a library)
19
- return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(text)}`;
20
- };
21
-
22
- const copyToClipboard = () => {
23
- if (value) {
24
- navigator.clipboard.writeText(value);
25
- }
26
- };
27
-
28
- if (readonly) {
29
- return (
30
- <div className="flex items-center gap-3">
31
- <span className="text-sm flex-1 truncate">{value || <EmptyValue />}</span>
32
- {value && (
33
- <Button
34
- type="button"
35
- variant="outline"
36
- size="sm"
37
- onClick={() => setShowQR(!showQR)}
38
- >
39
- <QrCode className="w-4 h-4 mr-2" />
40
- {showQR ? 'Hide' : 'Show'} QR
41
- </Button>
42
- )}
43
- </div>
44
- );
45
- }
46
-
47
- return (
48
- <div className="space-y-3">
49
- <div className="flex items-center gap-2">
50
- <Input
51
- // DOM pass-through onto the real focusable control (objectui#3318).
52
- {...toDomProps(props)}
53
- type="text"
54
- value={value || ''}
55
- onChange={(e) => onChange(e.target.value)}
56
- placeholder={config?.placeholder || 'Enter text for QR code'}
57
- disabled={readonly || props.disabled}
58
- className={props.className}
59
- // AFTER the spread so this widget's own computation wins: `error`
60
- // is the published validation slot (#3222), `!!undefined` →
61
- // explicit "false".
62
- aria-invalid={!!error}
63
- />
64
- {value && (
65
- <>
66
- <Button
67
- type="button"
68
- variant="outline"
69
- size="sm"
70
- onClick={copyToClipboard}
71
- >
72
- <Copy className="w-4 h-4" />
73
- </Button>
74
- <Button
75
- type="button"
76
- variant="outline"
77
- size="sm"
78
- onClick={() => setShowQR(!showQR)}
79
- >
80
- <QrCode className="w-4 h-4" />
81
- </Button>
82
- </>
83
- )}
84
- </div>
85
-
86
- {showQR && value && (
87
- <div className="border rounded p-4 bg-white flex justify-center">
88
- <img
89
- src={getQRCodeUrl(value)}
90
- alt="QR Code"
91
- className="w-48 h-48"
92
- />
93
- </div>
94
- )}
95
-
96
- {value && (
97
- <p className="text-xs text-muted-foreground">
98
- QR code contains: {value.length} characters
99
- </p>
100
- )}
101
- </div>
102
- );
103
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Cascading / role-gated `radio` options (#2715) — parity with the single
11
- * `SelectField` (ADR-0058 / #2284).
12
- *
13
- * Exercises the observable widget behaviour: the dependency gate (a "select the
14
- * parent first" empty-state) and the scalar cascade clear (a value dropped when
15
- * the offered set no longer includes it). The per-option filtering itself is
16
- * unit-tested in `@object-ui/core` (`optionRules.test.ts`); here we prove the
17
- * radio group wires `dependentValues` + predicate scope into that resolver via
18
- * the shared `useCascadingOptions` hook.
19
- */
20
- import { describe, it, expect, vi, beforeEach } from 'vitest';
21
- import React from 'react';
22
- import { render, screen } from '@testing-library/react';
23
- import '@testing-library/jest-dom';
24
- import { PredicateScopeProvider } from '@object-ui/react';
25
- import { RadioField } from './RadioField';
26
-
27
- const provinceField = {
28
- name: 'province',
29
- type: 'radio',
30
- dependsOn: 'country',
31
- options: [
32
- { label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" },
33
- { label: 'California', value: 'ca', visibleWhen: "record.country == 'us'" },
34
- ],
35
- } as any;
36
-
37
- beforeEach(() => {
38
- vi.clearAllMocks();
39
- });
40
-
41
- describe('RadioField — dependency gating (#2715)', () => {
42
- it('gates with a "select parent first" hint while the controlling field is empty', () => {
43
- render(
44
- <RadioField
45
- value={undefined}
46
- onChange={vi.fn()}
47
- field={provinceField}
48
- {...({ name: 'province', dependentValues: {} } as any)}
49
- />,
50
- );
51
- const gate = screen.getByTestId('radio-empty-province');
52
- expect(gate).toHaveTextContent(/select country first/i);
53
- // Gated → no radios offered.
54
- expect(screen.queryAllByRole('radio')).toHaveLength(0);
55
- });
56
-
57
- it('unlocks the radios once the controlling field is set', () => {
58
- render(
59
- <RadioField
60
- value={undefined}
61
- onChange={vi.fn()}
62
- field={provinceField}
63
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
64
- />,
65
- );
66
- expect(screen.queryByTestId('radio-empty-province')).not.toBeInTheDocument();
67
- // Only the `cn` option is offered.
68
- expect(screen.getByRole('radio', { name: 'Zhejiang' })).toBeInTheDocument();
69
- expect(screen.queryByRole('radio', { name: 'California' })).not.toBeInTheDocument();
70
- });
71
- });
72
-
73
- describe('RadioField — cascade clear (#2715)', () => {
74
- it('clears a value the parent change no longer offers', () => {
75
- const onChange = vi.fn();
76
- render(
77
- <RadioField
78
- value={'ca'}
79
- onChange={onChange}
80
- field={provinceField}
81
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
82
- />,
83
- );
84
- // 'ca' is a US province — under country=cn it is not offered, so it is dropped.
85
- expect(onChange).toHaveBeenCalledWith(undefined);
86
- });
87
-
88
- it('keeps a value that is still offered', () => {
89
- const onChange = vi.fn();
90
- render(
91
- <RadioField
92
- value={'zj'}
93
- onChange={onChange}
94
- field={provinceField}
95
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
96
- />,
97
- );
98
- expect(onChange).not.toHaveBeenCalled();
99
- });
100
- });
101
-
102
- describe('RadioField — role / context gating (#2715)', () => {
103
- const tierField = {
104
- name: 'tier',
105
- type: 'radio',
106
- options: [
107
- { label: 'Standard', value: 'standard' },
108
- { label: 'Admin only', value: 'admin_only', visibleWhen: "'admin' in current_user.positions" },
109
- ],
110
- } as any;
111
-
112
- it('clears an admin-only value for a non-admin (offered set excludes it)', () => {
113
- const onChange = vi.fn();
114
- render(
115
- <PredicateScopeProvider scope={{ current_user: { positions: ['sales'] } }}>
116
- <RadioField
117
- value={'admin_only'}
118
- onChange={onChange}
119
- field={tierField}
120
- {...({ name: 'tier', dependentValues: {} } as any)}
121
- />
122
- </PredicateScopeProvider>,
123
- );
124
- expect(onChange).toHaveBeenCalledWith(undefined);
125
- });
126
-
127
- it('keeps an admin-only value for an admin', () => {
128
- const onChange = vi.fn();
129
- render(
130
- <PredicateScopeProvider scope={{ current_user: { positions: ['admin'] } }}>
131
- <RadioField
132
- value={'admin_only'}
133
- onChange={onChange}
134
- field={tierField}
135
- {...({ name: 'tier', dependentValues: {} } as any)}
136
- />
137
- </PredicateScopeProvider>,
138
- );
139
- expect(onChange).not.toHaveBeenCalled();
140
- });
141
- });
@@ -1,111 +0,0 @@
1
- import React, { useId, useEffect } from 'react';
2
- import { RadioGroup, RadioGroupItem, Label, EmptyValue } from '@object-ui/components';
3
- import { isValueStillOffered, type OptionLike } from '@object-ui/core';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { OptionsEmptyState } from './OptionsEmptyState';
7
- import { useCascadingOptions } from './useCascadingOptions';
8
-
9
- type Option = OptionLike;
10
-
11
- /**
12
- * RadioField - choose exactly one value from a fixed option set, rendered as a
13
- * radio group. The stored value is the selected option value (string). Used for
14
- * the `radio` field type.
15
- *
16
- * Like the single `SelectField`, options support per-option `visibleWhen`
17
- * cascading + `dependsOn` gating (ADR-0058 / #2715), resolved through the shared
18
- * {@link useCascadingOptions} hook: the offered radios narrow against the live
19
- * form record + `current_user`, the control is gated behind a "select the parent
20
- * first" hint while a dependency is empty, and a value no longer offered (parent
21
- * changed / predicate flipped) is cleared.
22
- */
23
- export function RadioField({
24
- value,
25
- onChange,
26
- field,
27
- readonly,
28
- className,
29
- dependentValues,
30
- dependsOn: dependsOnProp,
31
- emptyHint,
32
- dataSource: _dataSource,
33
- error,
34
- ...props
35
- }: FieldWidgetComponentProps<string>) {
36
- const config = field as any;
37
- const rawOptions: Option[] = config?.options || [];
38
- const groupId = useId();
39
- const fieldName = props.name || config?.name || props.id || '';
40
-
41
- const dependsOn = config?.dependsOn ?? dependsOnProp;
42
- const { options, gated, dependsOnFields } = useCascadingOptions<Option>(
43
- rawOptions,
44
- dependsOn,
45
- dependentValues,
46
- );
47
-
48
- // Cascade clear: once the offered set no longer includes the current value
49
- // (parent changed / predicate flipped), drop it so no stale pair persists.
50
- useEffect(() => {
51
- if (readonly) return;
52
- if (value === undefined || value === null || (value as unknown) === '') return;
53
- if (!isValueStillOffered(value, options)) onChange?.(undefined as unknown as string);
54
- // eslint-disable-next-line react-hooks/exhaustive-deps
55
- }, [options, gated]);
56
-
57
- if (readonly) {
58
- if (value == null || value === '') return <EmptyValue />;
59
- // Label from the raw set so a stored value hidden by `visibleWhen` still
60
- // renders its label rather than a bare id.
61
- const opt = rawOptions.find((o) => o.value === value);
62
- return <span className="text-sm">{opt?.label || String(value)}</span>;
63
- }
64
-
65
- // No offered options is unfillable — surface a legible state instead of an
66
- // empty radio group: the host's `emptyHint` when it computed one, else this
67
- // widget's own translated copy. Shared with the select / multiselect /
68
- // checkboxes so the four cannot drift again (objectui#3231).
69
- if (options.length === 0) {
70
- return (
71
- <OptionsEmptyState
72
- emptyHint={emptyHint}
73
- gated={gated}
74
- dependsOnFields={dependsOnFields}
75
- testId={fieldName ? `radio-empty-${fieldName}` : undefined}
76
- className="min-h-9"
77
- />
78
- );
79
- }
80
-
81
- return (
82
- // DOM pass-through onto the radiogroup (objectui#3318). Unlike Radix
83
- // `Select.Root` (#3306) this Root IS a real DOM element — a
84
- // `<div role="radiogroup">` — and `radiogroup` is exactly the role
85
- // WAI-ARIA designates to carry `aria-invalid` for a set of radios
86
- // (`radio` itself does not support it): the group's state is announced
87
- // when focus lands on any radio inside it.
88
- <RadioGroup
89
- {...toDomProps(props)}
90
- value={value ?? ''}
91
- onValueChange={onChange}
92
- disabled={props.disabled}
93
- className={className}
94
- // AFTER the spread so this widget's own computation wins (#3222).
95
- aria-invalid={!!error}
96
- >
97
- {options.map((opt) => {
98
- // Radix speaks strings — stringify the (possibly numeric,
99
- // #3090-widened) authored value at this boundary.
100
- const value = String(opt.value);
101
- const id = `${groupId}-${value}`;
102
- return (
103
- <div key={value} className="flex items-center space-x-2">
104
- <RadioGroupItem value={value} id={id} data-testid={`radio-option-${value}`} />
105
- <Label htmlFor={id} className="font-normal">{opt.label}</Label>
106
- </div>
107
- );
108
- })}
109
- </RadioGroup>
110
- );
111
- }
@@ -1,81 +0,0 @@
1
- import React from 'react';
2
- import { Star } from 'lucide-react';
3
- import { cn } from '@object-ui/components';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
-
7
- /**
8
- * Rating field widget - provides a star rating input
9
- * Supports numeric values from 0 to max (default 5)
10
- */
11
- export function RatingField({ value, onChange, field, readonly, className, error, ...props }: FieldWidgetComponentProps<number>) {
12
- // Get rating-specific configuration from field metadata
13
- const ratingField = field as any;
14
- const max = ratingField?.max ?? 5;
15
- const currentValue = value ?? 0;
16
-
17
- const [hoverValue, setHoverValue] = React.useState<number | null>(null);
18
-
19
- const displayValue = hoverValue !== null ? hoverValue : currentValue;
20
-
21
- if (readonly) {
22
- return (
23
- <div className={cn("flex items-center gap-1", className)}>
24
- {Array.from({ length: max }, (_, i) => (
25
- <Star
26
- key={i}
27
- className={`w-5 h-5 ${
28
- i < currentValue
29
- ? 'fill-yellow-400 text-yellow-400'
30
- : 'text-muted-foreground'
31
- }`}
32
- />
33
- ))}
34
- <span className="ml-2 text-sm text-muted-foreground">
35
- {currentValue} / {max}
36
- </span>
37
- </div>
38
- );
39
- }
40
-
41
- // DOM pass-through (objectui#3318): the container carries the form
42
- // renderer's id / aria-describedby, but NOT its `aria-invalid` — a plain
43
- // wrapper div is not where assistive tech reads the invalid state. That
44
- // state goes onto every focusable star button below, computed from the
45
- // published `error` slot (#3222). `name` is withheld too: it is only
46
- // DOM-legal on form controls, and on this div it is exactly the leak
47
- // #3291 sweeps for.
48
- const {
49
- 'aria-invalid': _hostAriaInvalid,
50
- name: _domName,
51
- ...groupDomProps
52
- } = toDomProps(props);
53
-
54
- return (
55
- <div {...groupDomProps} className={cn("flex items-center gap-1", className)}>
56
- {Array.from({ length: max }, (_, i) => (
57
- <button
58
- key={i}
59
- type="button"
60
- onClick={() => onChange(i + 1)}
61
- onMouseEnter={() => setHoverValue(i + 1)}
62
- onMouseLeave={() => setHoverValue(null)}
63
- className="focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded"
64
- disabled={readonly || props.disabled}
65
- aria-invalid={!!error}
66
- >
67
- <Star
68
- className={`w-5 h-5 transition-colors ${
69
- i < displayValue
70
- ? 'fill-yellow-400 text-yellow-400 hover:fill-yellow-500 hover:text-yellow-500'
71
- : 'text-muted-foreground hover:text-yellow-400'
72
- }`}
73
- />
74
- </button>
75
- ))}
76
- <span className="ml-2 text-sm text-muted-foreground">
77
- {currentValue} / {max}
78
- </span>
79
- </div>
80
- );
81
- }