@dxos/react-ui-form 0.10.0 → 0.11.1

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 (182) hide show
  1. package/dist/lib/index.mjs +4064 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/translations.mjs +77 -0
  4. package/dist/lib/translations.mjs.map +1 -0
  5. package/dist/lib/types.mjs +0 -0
  6. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +1 -0
  7. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Form/Card.stories.d.ts +88 -0
  9. package/dist/types/src/components/Form/Card.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/Form/Form.d.ts +7 -7
  11. package/dist/types/src/components/Form/Form.stories.d.ts +21 -0
  12. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.theme.d.ts +6 -0
  14. package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/FormControls.d.ts +7 -7
  16. package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/FormField/FormField.d.ts +1 -1
  18. package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormField/FormRow.d.ts +17 -3
  20. package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts +0 -6
  22. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
  24. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts +7 -0
  26. package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts +1 -0
  28. package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts +1 -0
  30. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts +1 -0
  33. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts +1 -0
  35. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts +1 -0
  38. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +0 -11
  40. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts +1 -0
  42. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts +1 -0
  44. package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts.map +1 -1
  45. package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts +12 -0
  46. package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -1
  48. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts +1 -0
  49. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -1
  50. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts +1 -11
  51. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
  52. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts +1 -0
  53. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts +13 -0
  55. package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts +1 -0
  57. package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts.map +1 -1
  58. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +1 -13
  59. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
  60. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts +1 -0
  61. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts +15 -0
  63. package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts.map +1 -0
  64. package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts +1 -0
  65. package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts.map +1 -1
  66. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.d.ts.map +1 -1
  67. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts +1 -0
  68. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts.map +1 -1
  69. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts +1 -0
  70. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts.map +1 -1
  71. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts +1 -0
  72. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts.map +1 -1
  73. package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts +1 -0
  74. package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts.map +1 -1
  75. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +1 -1
  76. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -1
  77. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +0 -27
  78. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
  79. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +1 -0
  80. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
  81. package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -0
  82. package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
  83. package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts +29 -0
  84. package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +10 -1
  86. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
  87. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +2 -0
  88. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -1
  89. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +8 -23
  90. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  91. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +1 -0
  92. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  93. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +1 -1
  94. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  95. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +1 -0
  96. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
  97. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +1 -0
  98. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -1
  99. package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -1
  100. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -0
  101. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
  102. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +9 -8
  103. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -1
  104. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +1 -0
  105. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  106. package/dist/types/src/hooks/useFormHandler.d.ts +6 -2
  107. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  108. package/dist/types/src/hooks/useFormHandler.test.d.ts +2 -0
  109. package/dist/types/src/hooks/useFormHandler.test.d.ts.map +1 -0
  110. package/dist/types/src/testing/TestLayout.d.ts +1 -1
  111. package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
  112. package/dist/types/src/translations.d.ts +1 -0
  113. package/dist/types/src/translations.d.ts.map +1 -1
  114. package/dist/types/src/types.d.ts +5 -0
  115. package/dist/types/src/types.d.ts.map +1 -1
  116. package/dist/types/src/util/index.d.ts +0 -1
  117. package/dist/types/src/util/index.d.ts.map +1 -1
  118. package/dist/types/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +39 -42
  120. package/src/components/Form/Card.stories.tsx +93 -0
  121. package/src/components/Form/Form.stories.tsx +95 -3
  122. package/src/components/Form/Form.theme.ts +14 -8
  123. package/src/components/Form/Form.tsx +2 -2
  124. package/src/components/Form/FormControls.tsx +32 -13
  125. package/src/components/Form/FormField/FormField.tsx +11 -3
  126. package/src/components/Form/FormField/FormRow.tsx +20 -1
  127. package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +3 -46
  128. package/src/components/Form/FormField/fields/ArrayField/default-value.ts +49 -0
  129. package/src/components/Form/FormField/fields/ArrayField/index.ts +2 -0
  130. package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +12 -2
  131. package/src/components/Form/FormField/fields/DateField/DateField.tsx +10 -3
  132. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +1 -1
  133. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +2 -12
  134. package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +1 -1
  135. package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +1 -36
  136. package/src/components/Form/FormField/fields/NumberField/index.ts +2 -0
  137. package/src/components/Form/FormField/fields/NumberField/numeric-constraints.ts +43 -0
  138. package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +1 -0
  139. package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +2 -2
  140. package/src/components/Form/FormField/fields/RefField/RefField.test.ts +1 -1
  141. package/src/components/Form/FormField/fields/RefField/RefField.tsx +3 -45
  142. package/src/components/Form/FormField/fields/RefField/find-ref-option.ts +56 -0
  143. package/src/components/Form/FormField/fields/RefField/index.ts +2 -0
  144. package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +2 -67
  145. package/src/components/Form/FormField/fields/SelectField/create-select-field.tsx +80 -0
  146. package/src/components/Form/FormField/fields/SelectField/index.ts +2 -0
  147. package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +2 -1
  148. package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +11 -9
  149. package/src/components/Form/FormLayout/FormLayout.test.ts +1 -1
  150. package/src/components/Form/FormLayout/FormLayout.tsx +1 -86
  151. package/src/components/Form/FormLayout/index.ts +2 -0
  152. package/src/components/Form/FormLayout/resolve-layout-field.ts +98 -0
  153. package/src/components/ObjectForm/ObjectForm.stories.tsx +93 -3
  154. package/src/components/ObjectForm/ObjectForm.tsx +22 -4
  155. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +1 -1
  156. package/src/components/ObjectPicker/ObjectPicker.tsx +3 -3
  157. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
  158. package/src/components/ObjectProperties/ObjectProperties.tsx +7 -1
  159. package/src/components/ObjectTree/ObjectTree.stories.tsx +1 -1
  160. package/src/components/RefEditor/RefEditor.stories.tsx +9 -1
  161. package/src/components/RefEditor/RefEditor.tsx +42 -6
  162. package/src/components/RefEditor/ref-editor-extension.ts +168 -91
  163. package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -1
  164. package/src/components/ViewEditor/ViewEditor.tsx +9 -9
  165. package/src/hooks/useFormHandler.test.ts +85 -0
  166. package/src/hooks/useFormHandler.ts +118 -31
  167. package/src/translations.ts +1 -0
  168. package/src/types.ts +5 -0
  169. package/src/util/index.ts +0 -1
  170. package/dist/lib/browser/index.mjs +0 -4675
  171. package/dist/lib/browser/index.mjs.map +0 -7
  172. package/dist/lib/browser/meta.json +0 -1
  173. package/dist/lib/browser/translations.mjs +0 -87
  174. package/dist/lib/browser/translations.mjs.map +0 -7
  175. package/dist/lib/node-esm/index.mjs +0 -4677
  176. package/dist/lib/node-esm/index.mjs.map +0 -7
  177. package/dist/lib/node-esm/meta.json +0 -1
  178. package/dist/lib/node-esm/translations.mjs +0 -89
  179. package/dist/lib/node-esm/translations.mjs.map +0 -7
  180. package/dist/types/src/util/formatting.d.ts +0 -15
  181. package/dist/types/src/util/formatting.d.ts.map +0 -1
  182. package/src/util/formatting.ts +0 -171
@@ -23,6 +23,7 @@ import { useFormValues } from '../../../../../hooks';
23
23
  import { getFormProperties } from '../../../../../util';
24
24
  import { CompactIconButton, FormField, type FormFieldProps } from '../../FormField';
25
25
  import { FormFieldHeader } from '../../FormFieldHeader';
26
+ import { getDefaultValue } from './default-value';
26
27
 
27
28
  // Synthetic id assigned to each row when rendering an ordered list. Plain form
28
29
  // values have no stable identity, so drag-and-drop (which requires a stable key
@@ -185,13 +186,7 @@ export const ArrayField = ({
185
186
  // TODO(burdon): Hacky.
186
187
  // An owned inline object form must establish its own column context: otherwise it inherits the parent
187
188
  // form's `--dx-col` (center) placement and collapses into the array row's narrow second track.
188
- return createInline ? (
189
- <div role='none' className='w-full min-w-0 [--dx-col:auto]'>
190
- {field}
191
- </div>
192
- ) : (
193
- field
194
- );
189
+ return createInline ? <div className='w-full min-w-0 [--dx-col:auto]'>{field}</div> : field;
195
190
  };
196
191
 
197
192
  const header = (layout !== 'static' || (values && values.length > 0)) && (
@@ -260,7 +255,7 @@ export const ArrayField = ({
260
255
  return (
261
256
  <>
262
257
  {header}
263
- <div className='flex flex-col gap-2'>
258
+ <div className='flex flex-col gap-2 pb-2'>
264
259
  {values?.map((_, index) => {
265
260
  const isLast = index === values.length - 1;
266
261
  return (
@@ -288,41 +283,3 @@ export const ArrayField = ({
288
283
  };
289
284
 
290
285
  ArrayField.displayName = 'Form.ArrayField';
291
-
292
- /**
293
- * Returns the default empty value for a given AST.
294
- * Used for initializing new array values etc.
295
- */
296
- // TODO(wittjosiah): Factor out?
297
- export const getDefaultValue = (ast?: SchemaAST.AST): any => {
298
- switch (ast?._tag) {
299
- case 'StringKeyword': {
300
- return '';
301
- }
302
- case 'NumberKeyword': {
303
- return 0;
304
- }
305
- case 'BooleanKeyword': {
306
- return false;
307
- }
308
- case 'Suspend': {
309
- return getDefaultValue(ast.f());
310
- }
311
- case 'Refinement': {
312
- // Use minimum from JSON schema annotation (e.g. Schema.between(1, 31)) as the default
313
- // so new array items start within the valid range.
314
- const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(ast));
315
- if (jsonSchema != null && 'minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
316
- return jsonSchema.minimum;
317
- }
318
- return getDefaultValue(ast.from);
319
- }
320
- default: {
321
- if (ast && SchemaEx.isNestedType(ast)) {
322
- return {};
323
- } else {
324
- throw new Error(`Unsupported type: ${ast?._tag}`);
325
- }
326
- }
327
- }
328
- };
@@ -0,0 +1,49 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import * as SchemaAST from 'effect/SchemaAST';
7
+
8
+ import { SchemaEx } from '@dxos/effect';
9
+
10
+ // Kept out of `ArrayField.tsx`: react-refresh only fast-refreshes a module whose
11
+ // exports are all components, so values exported beside them force a full page reload on every edit.
12
+
13
+ /**
14
+ * Returns the default empty value for a given AST.
15
+ * Used for initializing new array values etc.
16
+ */
17
+ // TODO(wittjosiah): Factor out?
18
+ export const getDefaultValue = (ast?: SchemaAST.AST): any => {
19
+ switch (ast?._tag) {
20
+ case 'StringKeyword': {
21
+ return '';
22
+ }
23
+ case 'NumberKeyword': {
24
+ return 0;
25
+ }
26
+ case 'BooleanKeyword': {
27
+ return false;
28
+ }
29
+ case 'Suspend': {
30
+ return getDefaultValue(ast.f());
31
+ }
32
+ case 'Refinement': {
33
+ // Use minimum from JSON schema annotation (e.g. Schema.between(1, 31)) as the default
34
+ // so new array items start within the valid range.
35
+ const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(ast));
36
+ if (jsonSchema != null && 'minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
37
+ return jsonSchema.minimum;
38
+ }
39
+ return getDefaultValue(ast.from);
40
+ }
41
+ default: {
42
+ if (ast && SchemaEx.isNestedType(ast)) {
43
+ return {};
44
+ } else {
45
+ throw new Error(`Unsupported type: ${ast?._tag}`);
46
+ }
47
+ }
48
+ }
49
+ };
@@ -2,4 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './default-value';
6
+
5
7
  export * from './ArrayField';
@@ -29,6 +29,9 @@ export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChan
29
29
  const values = useFormValues<AnyProperties>(ComboboxField.displayName);
30
30
  const ownKey = props.jsonPath;
31
31
  const [query, setQuery] = useState('');
32
+ // The stored value is often an opaque id; remember the selected option's label so the trigger shows
33
+ // it (the query-driven suggestions no longer include the option once selected).
34
+ const [selectedLabel, setSelectedLabel] = useState<string | undefined>();
32
35
 
33
36
  // When the combobox searches on its own field (its name is among `deps`), feed the live query to the
34
37
  // loader so remote suggestions track typing; otherwise the loader reads the committed sibling values.
@@ -54,7 +57,13 @@ export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChan
54
57
  );
55
58
  const hasExact = results.some((option) => option.value === trimmed);
56
59
 
57
- const handleValueChange = useCallback((next: string) => onValueChange(type, next), [onValueChange, type]);
60
+ const handleValueChange = useCallback(
61
+ (next: string) => {
62
+ setSelectedLabel((data ?? []).find((option) => option.value === next)?.label);
63
+ onValueChange(type, next);
64
+ },
65
+ [onValueChange, type, data],
66
+ );
58
67
  const handleOpenChange = useCallback((open: boolean) => {
59
68
  if (!open) {
60
69
  setQuery('');
@@ -64,12 +73,13 @@ export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChan
64
73
  return (
65
74
  <FormRow<string>
66
75
  readonly={readonly}
67
- renderStatic={(value) => <p className='truncate min-w-0'>{value ?? ''}</p>}
76
+ renderStatic={(value) => <p className='truncate min-w-0'>{selectedLabel ?? value ?? ''}</p>}
68
77
  {...props}
69
78
  >
70
79
  {({ value = '' }) => (
71
80
  <Combobox.Root
72
81
  value={value}
82
+ displayValue={selectedLabel}
73
83
  onValueChange={handleValueChange}
74
84
  onOpenChange={handleOpenChange}
75
85
  placeholder={placeholder}
@@ -71,7 +71,7 @@ export const DateField = ({
71
71
  switch (format) {
72
72
  case Format.TypeFormat.Date:
73
73
  return (
74
- <div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch'>
74
+ <div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch tabular-nums'>
75
75
  <Input.Date
76
76
  classNames='min-w-0 overflow-hidden'
77
77
  disabled={readonly}
@@ -82,11 +82,18 @@ export const DateField = ({
82
82
  </div>
83
83
  );
84
84
  case Format.TypeFormat.Time:
85
- return <Input.Time disabled={!!readonly} value={value ?? ''} onValueChange={handleSimpleChange} />;
85
+ return (
86
+ <Input.Time
87
+ classNames='tabular-nums'
88
+ disabled={!!readonly}
89
+ value={value ?? ''}
90
+ onValueChange={handleSimpleChange}
91
+ />
92
+ );
86
93
  case Format.TypeFormat.DateTime:
87
94
  default:
88
95
  return (
89
- <div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch'>
96
+ <div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch tabular-nums'>
90
97
  <Input.DateTime
91
98
  classNames='min-w-0 overflow-hidden'
92
99
  disabled={readonly}
@@ -8,7 +8,7 @@ import React, { useMemo } from 'react';
8
8
  import { expect, userEvent } from 'storybook/test';
9
9
 
10
10
  import { Filter, Format, Obj, Ref } from '@dxos/echo';
11
- import { useQuery } from '@dxos/react-client/echo';
11
+ import { useQuery } from '@dxos/echo-react';
12
12
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
13
13
  import { Panel } from '@dxos/react-ui';
14
14
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
@@ -17,16 +17,6 @@ import { type FormFieldRendererProps } from '#types';
17
17
 
18
18
  import { FormRow } from '../../FormRow';
19
19
 
20
- // Mirror the focus treatment of `Input` (default variant): a `focus-ring-subtle` ring of `dx-focus-line`
21
- // width rather than a recolored 1px border, and suppress the editor wrapper's `accent-bg` border so a
22
- // second click never turns the outline blue. Keyed on `focus-within` because focus lands on the inner
23
- // CodeMirror contenteditable, not this wrapper.
24
- const editorClassNames = [
25
- 'min-h-[6lh] transition-colors rounded-xs p-1 px-2 bg-input-surface border border-input-separator',
26
- 'focus:border-separator focus-within:border-separator focus-within:bg-focus-surface',
27
- 'focus-within:ring-2 focus-within:ring-offset-0 focus-within:ring-focus-ring-subtle focus-within:z-[1]',
28
- ];
29
-
30
20
  /**
31
21
  * Form field that edits a markdown value in a CodeMirror editor.
32
22
  * Supports two backings:
@@ -116,7 +106,7 @@ const RefMarkdownEditor = ({ reference, placeholder, readonly }: RefMarkdownEdit
116
106
 
117
107
  return (
118
108
  <Editor.Root>
119
- <Editor.View classNames={editorClassNames} extensions={extensions} />
109
+ <Editor.View classNames='dx-input' extensions={extensions} />
120
110
  </Editor.Root>
121
111
  );
122
112
  };
@@ -135,7 +125,7 @@ const StringMarkdownEditor = ({ value, placeholder, readonly, onChange }: String
135
125
  return (
136
126
  <Editor.Root>
137
127
  <Editor.View
138
- classNames={editorClassNames}
128
+ classNames='dx-input min-h-[6lh]'
139
129
  extensions={extensions}
140
130
  value={value}
141
131
  onChange={readonly ? undefined : handleChange}
@@ -5,7 +5,7 @@
5
5
  import * as Schema from 'effect/Schema';
6
6
  import { describe, test } from 'vitest';
7
7
 
8
- import { getNumericConstraints } from './NumberField';
8
+ import { getNumericConstraints } from './numeric-constraints';
9
9
 
10
10
  describe('getNumericConstraints', () => {
11
11
  test('reads min/max from Schema.between (bounds live on separate nested refinements)', ({ expect }) => {
@@ -2,8 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import * as Option from 'effect/Option';
6
- import * as SchemaAST from 'effect/SchemaAST';
7
5
  import React, { useCallback, useEffect, useState } from 'react';
8
6
 
9
7
  import { Input, type TextInputProps } from '@dxos/react-ui';
@@ -12,40 +10,7 @@ import { safeParseFloat } from '@dxos/util';
12
10
  import { type FormFieldRendererProps } from '#types';
13
11
 
14
12
  import { FormRow } from '../../FormRow';
15
-
16
- /**
17
- * Extracts numeric constraints (`minimum`/`maximum` bounds and whether the value must be an integer)
18
- * by walking the chain of Refinement ASTs and reading each one's JSON schema annotation (e.g. produced
19
- * by `Schema.between` / `Schema.int`). Returns empty constraints when none are declared.
20
- */
21
- export const getNumericConstraints = (ast: SchemaAST.AST): { min?: number; max?: number; integer: boolean } => {
22
- let node: SchemaAST.AST | undefined = ast;
23
- let min: number | undefined;
24
- let max: number | undefined;
25
- let integer = false;
26
- // Nested refinements (e.g. `Schema.int()` + `Schema.between()`) each carry their own JSON schema
27
- // fragment, so accumulate across the chain rather than reading only the outermost node.
28
- while (node && SchemaAST.isRefinement(node)) {
29
- const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(node));
30
- if (jsonSchema != null) {
31
- // Stacked refinements intersect: keep the strictest bound (largest min, smallest max).
32
- if ('minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
33
- min = min === undefined ? jsonSchema.minimum : Math.max(min, jsonSchema.minimum);
34
- }
35
- if ('maximum' in jsonSchema && typeof jsonSchema.maximum === 'number') {
36
- max = max === undefined ? jsonSchema.maximum : Math.min(max, jsonSchema.maximum);
37
- }
38
- if (
39
- ('type' in jsonSchema && jsonSchema.type === 'integer') ||
40
- ('multipleOf' in jsonSchema && jsonSchema.multipleOf === 1)
41
- ) {
42
- integer = true;
43
- }
44
- }
45
- node = node.from;
46
- }
47
- return { min, max, integer };
48
- };
13
+ import { getNumericConstraints } from './numeric-constraints';
49
14
 
50
15
  export const NumberField = ({
51
16
  type,
@@ -2,4 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './numeric-constraints';
6
+
5
7
  export * from './NumberField';
@@ -0,0 +1,43 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import * as SchemaAST from 'effect/SchemaAST';
7
+
8
+ // Kept out of `NumberField.tsx`: react-refresh only fast-refreshes a module whose
9
+ // exports are all components, so values exported beside them force a full page reload on every edit.
10
+
11
+ /**
12
+ * Extracts numeric constraints (`minimum`/`maximum` bounds and whether the value must be an integer)
13
+ * by walking the chain of Refinement ASTs and reading each one's JSON schema annotation (e.g. produced
14
+ * by `Schema.between` / `Schema.int`). Returns empty constraints when none are declared.
15
+ */
16
+ export const getNumericConstraints = (ast: SchemaAST.AST): { min?: number; max?: number; integer: boolean } => {
17
+ let node: SchemaAST.AST | undefined = ast;
18
+ let min: number | undefined;
19
+ let max: number | undefined;
20
+ let integer = false;
21
+ // Nested refinements (e.g. `Schema.int()` + `Schema.between()`) each carry their own JSON schema
22
+ // fragment, so accumulate across the chain rather than reading only the outermost node.
23
+ while (node && SchemaAST.isRefinement(node)) {
24
+ const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(node));
25
+ if (jsonSchema != null) {
26
+ // Stacked refinements intersect: keep the strictest bound (largest min, smallest max).
27
+ if ('minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
28
+ min = min === undefined ? jsonSchema.minimum : Math.max(min, jsonSchema.minimum);
29
+ }
30
+ if ('maximum' in jsonSchema && typeof jsonSchema.maximum === 'number') {
31
+ max = max === undefined ? jsonSchema.maximum : Math.min(max, jsonSchema.maximum);
32
+ }
33
+ if (
34
+ ('type' in jsonSchema && jsonSchema.type === 'integer') ||
35
+ ('multipleOf' in jsonSchema && jsonSchema.multipleOf === 1)
36
+ ) {
37
+ integer = true;
38
+ }
39
+ }
40
+ node = node.from;
41
+ }
42
+ return { min, max, integer };
43
+ };
@@ -29,6 +29,7 @@ export const PasswordField = ({
29
29
  <Input.TextInput
30
30
  type='password'
31
31
  noAutoFill
32
+ spellCheck={false}
32
33
  disabled={!!readonly}
33
34
  placeholder={placeholder}
34
35
  value={value}
@@ -15,7 +15,7 @@ import { IconButton, Input, useTranslation } from '@dxos/react-ui';
15
15
  import { translationKey } from '#translations';
16
16
 
17
17
  import { omitId } from '../../../../../util';
18
- import { FormContent, FormFieldSetController, FormRoot } from '../../../FormControls';
18
+ import { FormContent, FormFieldSetContainer, FormRoot } from '../../../FormControls';
19
19
  import { FormFieldLabel } from '../../FormRow';
20
20
  import { presentationFor } from '../../presentation';
21
21
  import { type RefFieldProps } from './RefField';
@@ -138,7 +138,7 @@ const InlineForm = ({ reference, db, readonly, useType = defaultUseType }: Inlin
138
138
  return (
139
139
  <FormRoot db={db} schema={formSchema} defaultValues={defaultValues as any} onValuesChanged={handleChange}>
140
140
  <FormContent>
141
- <FormFieldSetController collapsible readonly={readonly} />
141
+ <FormFieldSetContainer collapsible readonly={readonly} />
142
142
  </FormContent>
143
143
  </FormRoot>
144
144
  );
@@ -6,7 +6,7 @@ import { describe, test } from 'vitest';
6
6
 
7
7
  import { type RefOption } from '#types';
8
8
 
9
- import { findRefOption } from './RefField';
9
+ import { findRefOption } from './find-ref-option';
10
10
 
11
11
  // Sample qualified self URIs as produced by an in-space object (Entity.getURI / `echo://<space>/<id>`).
12
12
  const SPACE_ID = 'BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE';
@@ -9,63 +9,21 @@ import { Entity, Filter, Obj, Query, Ref, Scope, Type } from '@dxos/echo';
9
9
  import { useType as defaultUseType, useQuery } from '@dxos/echo-react';
10
10
  import { ANY_OBJECT_TYPENAME, ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
11
11
  import { SchemaEx } from '@dxos/effect';
12
- import { DXN, EID, URI } from '@dxos/keys';
12
+ import { DXN, URI } from '@dxos/keys';
13
13
  import { DxAnchor } from '@dxos/lit-ui/react';
14
14
  import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
15
15
  import { ParentLabelAnnotationId } from '@dxos/schema';
16
16
 
17
17
  import { translationKey } from '#translations';
18
- import { type CreateOptions, type FormFieldRendererProps, type RefFieldDataProps, type RefOption } from '#types';
18
+ import { type CreateOptions, type FormFieldRendererProps, type RefFieldDataProps } from '#types';
19
19
 
20
20
  import { omitHiddenFormFields, omitId } from '../../../../../util';
21
21
  import { ObjectPicker } from '../../../../ObjectPicker';
22
22
  import { FormFieldLabel } from '../../FormRow';
23
23
  import { presentationFor } from '../../presentation';
24
+ import { findRefOption } from './find-ref-option';
24
25
 
25
26
  // TODO(burdon): Factor out.
26
- const isRefSnapshot = (val: any): val is { '/': string } => {
27
- return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
28
- };
29
-
30
- /**
31
- * Find the option a ref-like form value points at. Matches on the local (entity-id) form so a bare local EID
32
- * (`echo:/<id>`, produced by `Ref.make`) still resolves against an option keyed by the entity's qualified
33
- * self URI (`echo://<space>/<id>`). Returns `undefined` when the value is not a ref or no option matches.
34
- *
35
- * Comparing by entity id is only sound within one space (ids are unique there, not globally). Two EIDs that
36
- * both carry a space authority must therefore agree on it: a qualified value and a qualified option from
37
- * different spaces never match, even when their entity ids coincide.
38
- */
39
- export const findRefOption = (value: unknown, options: RefOption[]): RefOption | undefined => {
40
- const isRef = Ref.isRef(value);
41
- if (!isRef && !isRefSnapshot(value)) {
42
- return undefined;
43
- }
44
- const valueUri = isRef ? value.uri : value['/'];
45
- // Keyed/registry entities (skills, operations) are referenced by a named DXN rather than an
46
- // entity-id, so they carry no parseable EID; match those by direct URI equality against the option id.
47
- const directMatch = options.find((option) => option.id === valueUri);
48
- if (directMatch) {
49
- return directMatch;
50
- }
51
- const valueEid = EID.tryParse(valueUri);
52
- if (!valueEid) {
53
- return undefined;
54
- }
55
- const valueSpaceId = EID.getSpaceId(valueEid);
56
- const valueLocal = EID.toLocal(valueEid);
57
- return options.find((option) => {
58
- const optionEid = EID.tryParse(option.id);
59
- if (!optionEid) {
60
- return false;
61
- }
62
- const optionSpaceId = EID.getSpaceId(optionEid);
63
- if (valueSpaceId != null && optionSpaceId != null && valueSpaceId !== optionSpaceId) {
64
- return false;
65
- }
66
- return EID.equals(EID.toLocal(optionEid), valueLocal);
67
- });
68
- };
69
27
 
70
28
  const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (
71
29
  results,
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
6
+ import { Ref } from '@dxos/echo';
7
+ import { EID } from '@dxos/keys';
8
+
9
+ import { type RefOption } from '#types';
10
+
11
+ // Kept out of `RefField.tsx`: react-refresh only fast-refreshes a module whose
12
+ // exports are all components, so values exported beside them force a full page reload on every edit.
13
+
14
+ const isRefSnapshot = (val: any): val is { '/': string } => {
15
+ return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
16
+ };
17
+
18
+ /**
19
+ * Find the option a ref-like form value points at. Matches on the local (entity-id) form so a bare local EID
20
+ * (`echo:/<id>`, produced by `Ref.make`) still resolves against an option keyed by the entity's qualified
21
+ * self URI (`echo://<space>/<id>`). Returns `undefined` when the value is not a ref or no option matches.
22
+ *
23
+ * Comparing by entity id is only sound within one space (ids are unique there, not globally). Two EIDs that
24
+ * both carry a space authority must therefore agree on it: a qualified value and a qualified option from
25
+ * different spaces never match, even when their entity ids coincide.
26
+ */
27
+ export const findRefOption = (value: unknown, options: RefOption[]): RefOption | undefined => {
28
+ const isRef = Ref.isRef(value);
29
+ if (!isRef && !isRefSnapshot(value)) {
30
+ return undefined;
31
+ }
32
+ const valueUri = isRef ? value.uri : value['/'];
33
+ // Keyed/registry entities (skills, operations) are referenced by a named DXN rather than an
34
+ // entity-id, so they carry no parseable EID; match those by direct URI equality against the option id.
35
+ const directMatch = options.find((option) => option.id === valueUri);
36
+ if (directMatch) {
37
+ return directMatch;
38
+ }
39
+ const valueEid = EID.tryParse(valueUri);
40
+ if (!valueEid) {
41
+ return undefined;
42
+ }
43
+ const valueSpaceId = EID.getSpaceId(valueEid);
44
+ const valueLocal = EID.toLocal(valueEid);
45
+ return options.find((option) => {
46
+ const optionEid = EID.tryParse(option.id);
47
+ if (!optionEid) {
48
+ return false;
49
+ }
50
+ const optionSpaceId = EID.getSpaceId(optionEid);
51
+ if (valueSpaceId != null && optionSpaceId != null && valueSpaceId !== optionSpaceId) {
52
+ return false;
53
+ }
54
+ return EID.equals(EID.toLocal(optionEid), valueLocal);
55
+ });
56
+ };
@@ -2,5 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './find-ref-option';
6
+
5
7
  export * from './RefField';
6
8
  export * from './InlineRefField';
@@ -4,11 +4,10 @@
4
4
 
5
5
  import React, { useCallback } from 'react';
6
6
 
7
- import { invariant } from '@dxos/invariant';
8
7
  import { Icon, Select, type SelectRootProps } from '@dxos/react-ui';
9
8
  import { getStyles } from '@dxos/ui-theme';
10
9
 
11
- import { type FormFieldRenderer, type FormFieldRendererProps } from '#types';
10
+ import { type FormFieldRendererProps } from '#types';
12
11
 
13
12
  import { FormRow } from '../../FormRow';
14
13
 
@@ -33,7 +32,7 @@ export const SelectField = ({ type, readonly, placeholder, options, onValueChang
33
32
  {...props}
34
33
  >
35
34
  {({ value }) => (
36
- <Select.Root value={value} onValueChange={handleValueChange}>
35
+ <Select.Root value={value} onValueChange={handleValueChange} disabled={!!readonly}>
37
36
  <Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
38
37
  {options?.length !== 0 && (
39
38
  <Select.Portal>
@@ -70,67 +69,3 @@ export type SelectFieldOption = {
70
69
  value: string;
71
70
  label?: string;
72
71
  };
73
-
74
- export type CreateSelectFieldOptions = {
75
- /** Options to display. Strings are used as both value and label. */
76
- options: ReadonlyArray<string | SelectFieldOption>;
77
- /** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
78
- defaultLabel?: string | null;
79
- };
80
-
81
- /**
82
- * Factory for a `fieldMap` entry (keyed by JSON path) that renders a Select over a fixed list of
83
- * options, with an optional sentinel mapping to `undefined`. Use for fields whose choices come from a
84
- * runtime list rather than the schema (e.g. available model ids).
85
- */
86
- export const createSelectField = ({
87
- options,
88
- defaultLabel = 'Default',
89
- }: CreateSelectFieldOptions): FormFieldRenderer => {
90
- const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
91
- const hasDefault = defaultLabel !== null;
92
- // The sentinel maps to `undefined`. Radix forbids an empty-string `Select.Item` value, so use a
93
- // non-empty placeholder; reserved (a real option with this value would be unrepresentable).
94
- const sentinel = '__default__';
95
- invariant(
96
- !normalized.some((option) => option.value === sentinel),
97
- `createSelectField: option value '${sentinel}' is reserved.`,
98
- );
99
-
100
- return ({ type, readonly, onValueChange, ...props }: FormFieldRendererProps<string | undefined>) => (
101
- <FormRow<string>
102
- readonly={readonly}
103
- renderStatic={(value) => (
104
- <p className='truncate min-w-0'>
105
- {normalized.find((option) => option.value === value)?.label ?? String(value ?? '')}
106
- </p>
107
- )}
108
- {...props}
109
- >
110
- {({ value }) => (
111
- <Select.Root
112
- disabled={!!readonly}
113
- value={value ?? sentinel}
114
- onValueChange={(next) => onValueChange(type, hasDefault && next === sentinel ? undefined : next)}
115
- >
116
- <Select.TriggerButton classNames='w-full' disabled={!!readonly} />
117
- {normalized.length > 0 && (
118
- <Select.Portal>
119
- <Select.Content>
120
- <Select.Viewport>
121
- {hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
122
- {normalized.map((option) => (
123
- <Select.Option key={option.value} value={option.value}>
124
- {option.label ?? option.value}
125
- </Select.Option>
126
- ))}
127
- </Select.Viewport>
128
- <Select.Arrow />
129
- </Select.Content>
130
- </Select.Portal>
131
- )}
132
- </Select.Root>
133
- )}
134
- </FormRow>
135
- );
136
- };