@object-ui/fields 3.3.0 → 3.3.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 (80) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/README.md +24 -0
  3. package/dist/{LookupField-BHr7uTX1.cjs → LookupField-CiIjeL_i.cjs} +1 -1
  4. package/dist/{LookupField-B6MOSQVJ.js → LookupField-De-GqsfT.js} +176 -179
  5. package/dist/index.cjs +3 -3
  6. package/dist/index.js +633 -655
  7. package/package.json +34 -9
  8. package/src/__tests__/cell-renderers.test.tsx +39 -18
  9. package/src/__tests__/datetime-cell.test.tsx +2 -2
  10. package/src/__tests__/datetime-timezone.test.tsx +9 -9
  11. package/src/__tests__/validation-feedback.test.tsx +8 -8
  12. package/src/datetime-widgets.test.tsx +1 -1
  13. package/src/index.tsx +54 -28
  14. package/src/record-picker.test.tsx +30 -23
  15. package/src/remaining-widgets.test.tsx +1 -1
  16. package/src/widgets/AddressField.tsx +4 -3
  17. package/src/widgets/AutoNumberField.tsx +5 -3
  18. package/src/widgets/CodeField.tsx +2 -2
  19. package/src/widgets/ColorField.tsx +2 -2
  20. package/src/widgets/CurrencyField.tsx +2 -2
  21. package/src/widgets/DateField.tsx +2 -2
  22. package/src/widgets/DateTimeField.tsx +2 -2
  23. package/src/widgets/EmailField.tsx +2 -2
  24. package/src/widgets/FileField.tsx +2 -2
  25. package/src/widgets/FormulaField.tsx +14 -12
  26. package/src/widgets/GeolocationField.tsx +4 -3
  27. package/src/widgets/GridField.tsx +3 -3
  28. package/src/widgets/ImageField.tsx +2 -2
  29. package/src/widgets/LocationField.tsx +2 -2
  30. package/src/widgets/LookupField.tsx +3 -5
  31. package/src/widgets/NumberField.tsx +2 -2
  32. package/src/widgets/ObjectField.tsx +2 -2
  33. package/src/widgets/PercentField.tsx +2 -2
  34. package/src/widgets/PhoneField.tsx +2 -2
  35. package/src/widgets/QRCodeField.tsx +2 -2
  36. package/src/widgets/RichTextField.tsx +2 -2
  37. package/src/widgets/SelectField.tsx +9 -7
  38. package/src/widgets/SummaryField.tsx +12 -10
  39. package/src/widgets/TextAreaField.tsx +2 -2
  40. package/src/widgets/TextField.tsx +2 -2
  41. package/src/widgets/TimeField.tsx +2 -2
  42. package/src/widgets/UrlField.tsx +2 -2
  43. package/src/widgets/UserField.tsx +2 -2
  44. package/src/widgets/VectorField.tsx +2 -1
  45. package/dist/AddressField-Do46PdqT.cjs +0 -1
  46. package/dist/AutoNumberField-cS2tsxd-.cjs +0 -1
  47. package/dist/AvatarField-CxhGS3wF.cjs +0 -1
  48. package/dist/BooleanField-BgkbTHPY.cjs +0 -1
  49. package/dist/CodeField-DE3dLruS.cjs +0 -1
  50. package/dist/ColorField-DBTFFYpv.cjs +0 -1
  51. package/dist/CurrencyField-C28LkJts.cjs +0 -1
  52. package/dist/DateField-WGWwddDF.cjs +0 -1
  53. package/dist/DateTimeField-BYsZs8f7.cjs +0 -1
  54. package/dist/EmailField-1PW03WW8.cjs +0 -1
  55. package/dist/FileField-BqZkp1O6.cjs +0 -1
  56. package/dist/FormulaField-MZ9le4RE.cjs +0 -1
  57. package/dist/GeolocationField-onbAOdIe.cjs +0 -1
  58. package/dist/GridField-D4OWfaU4.cjs +0 -1
  59. package/dist/ImageField-BZf-AL2e.cjs +0 -1
  60. package/dist/LocationField-DhGJtUzZ.cjs +0 -1
  61. package/dist/MasterDetailField-YTAU1hzg.cjs +0 -1
  62. package/dist/NumberField-BCk42NjA.cjs +0 -1
  63. package/dist/ObjectField-iYK5eab_.cjs +0 -1
  64. package/dist/PasswordField-BAReSDz7.cjs +0 -1
  65. package/dist/PercentField-nzACEXtC.cjs +0 -1
  66. package/dist/PhoneField-CvbkGpN3.cjs +0 -1
  67. package/dist/QRCodeField-D8fov03S.cjs +0 -1
  68. package/dist/RatingField-DBgj9cgE.cjs +0 -1
  69. package/dist/RichTextField-7VwN6GcU.cjs +0 -1
  70. package/dist/SelectField-BUOTiYaS.cjs +0 -1
  71. package/dist/SignatureField-DW_Cz6t9.cjs +0 -1
  72. package/dist/SliderField-Bgi3-TAD.cjs +0 -1
  73. package/dist/SummaryField-Ed2wJBvs.cjs +0 -1
  74. package/dist/TextAreaField-O503xJff.cjs +0 -1
  75. package/dist/TextField-BrwtPIQC.cjs +0 -1
  76. package/dist/TimeField-Bhoxv2yp.cjs +0 -1
  77. package/dist/UrlField-VFO089Gr.cjs +0 -1
  78. package/dist/UserField-BRe9SCCk.cjs +0 -1
  79. package/dist/VectorField-ByrWkiud.cjs +0 -1
  80. /package/dist/{useFieldTranslation-BiQad4ar.cjs → useFieldTranslation-BYPwtuNZ.cjs} +0 -0
@@ -1113,29 +1113,36 @@ describe('RecordPickerDialog — renderGrid slot', () => {
1113
1113
  expect(screen.getByTestId('custom-grid')).toBeInTheDocument();
1114
1114
  });
1115
1115
 
1116
- // renderGrid should have been called with grid slot props
1117
- expect(renderGrid).toHaveBeenCalledWith(
1118
- expect.objectContaining({
1119
- columns: expect.arrayContaining([
1120
- expect.objectContaining({ field: 'name' }),
1121
- ]),
1122
- records: expect.arrayContaining([
1123
- expect.objectContaining({ id: '1', name: 'Acme Corp' }),
1124
- ]),
1125
- loading: false,
1126
- totalCount: 1,
1127
- currentPage: 1,
1128
- pageSize: 10,
1129
- sortField: null,
1130
- sortDirection: 'asc',
1131
- onSort: expect.any(Function),
1132
- onPageChange: expect.any(Function),
1133
- onRowClick: expect.any(Function),
1134
- isSelected: expect.any(Function),
1135
- multiple: false,
1136
- idField: 'id',
1137
- }),
1138
- );
1116
+ // Wait for data to load, then check the last renderGrid call has records
1117
+ await waitFor(() => {
1118
+ expect(renderGrid).toHaveBeenLastCalledWith(
1119
+ expect.objectContaining({
1120
+ columns: expect.arrayContaining([
1121
+ expect.objectContaining({ field: 'name' }),
1122
+ ]),
1123
+ records: expect.arrayContaining([
1124
+ expect.objectContaining({ id: '1', name: 'Acme Corp' }),
1125
+ ]),
1126
+ loading: false,
1127
+ totalCount: 1,
1128
+ }),
1129
+ );
1130
+ });
1131
+
1132
+ // Verify all expected props are present in the last call
1133
+ const lastCallArgs = renderGrid.mock.calls[renderGrid.mock.calls.length - 1][0];
1134
+ expect(lastCallArgs).toMatchObject({
1135
+ currentPage: 1,
1136
+ pageSize: 10,
1137
+ sortField: null,
1138
+ sortDirection: 'asc',
1139
+ multiple: false,
1140
+ idField: 'id',
1141
+ });
1142
+ expect(lastCallArgs.onSort).toBeTypeOf('function');
1143
+ expect(lastCallArgs.onPageChange).toBeTypeOf('function');
1144
+ expect(lastCallArgs.onRowClick).toBeTypeOf('function');
1145
+ expect(lastCallArgs.isSelected).toBeTypeOf('function');
1139
1146
  });
1140
1147
 
1141
1148
  it('hides built-in table when renderGrid is provided', async () => {
@@ -37,7 +37,7 @@ describe('Remaining Field Widgets', () => {
37
37
 
38
38
  it('should render dash for null value', () => {
39
39
  render(<AutoNumberField {...baseProps} value={null as any} />);
40
- expect(screen.getByText('-')).toBeInTheDocument();
40
+ expect(screen.getByText('—')).toBeInTheDocument();
41
41
  });
42
42
  });
43
43
 
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input, Label } from '@object-ui/components';
2
+ import { Input, Label, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -34,11 +34,12 @@ export function AddressField({ value, onChange, field, readonly, ...props }: Fie
34
34
  [addr.state, addr.zipCode].filter(Boolean).join(' '),
35
35
  addr.country,
36
36
  ].filter(Boolean);
37
- return parts.join(', ') || '-';
37
+ return parts.join(', ');
38
38
  };
39
39
 
40
40
  if (readonly) {
41
- return <span className="text-sm">{formatAddress(address)}</span>;
41
+ const formatted = formatAddress(address);
42
+ return formatted ? <span className="text-sm">{formatted}</span> : <EmptyValue />;
42
43
  }
43
44
 
44
45
  return (
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { EmptyValue } from '@object-ui/components';
2
3
  import { FieldWidgetProps } from './types';
3
4
 
4
5
  /**
@@ -6,11 +7,12 @@ import { FieldWidgetProps } from './types';
6
7
  * Values are automatically generated by the system and cannot be edited
7
8
  */
8
9
  export function AutoNumberField({ value, field, ...props }: FieldWidgetProps<string | number>) {
9
- const displayValue = value != null ? String(value) : '-';
10
-
10
+ if (value == null) {
11
+ return <EmptyValue className={props.className} />;
12
+ }
11
13
  return (
12
14
  <span className={`text-sm font-mono text-gray-700 ${props.className || ''}`}>
13
- {displayValue}
15
+ {String(value)}
14
16
  </span>
15
17
  );
16
18
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Textarea, cn } from '@object-ui/components';
2
+ import { Textarea, cn, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -15,7 +15,7 @@ export function CodeField({ value, onChange, field, readonly, ...props }: FieldW
15
15
  if (readonly) {
16
16
  return (
17
17
  <pre className={cn("text-sm bg-muted p-2 rounded overflow-x-auto border", props.className)}>
18
- <code>{value || '-'}</code>
18
+ <code>{value || <EmptyValue />}</code>
19
19
  </pre>
20
20
  );
21
21
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -16,7 +16,7 @@ export function ColorField({ value, onChange, field, readonly, ...props }: Field
16
16
  className="w-6 h-6 rounded border border-input"
17
17
  style={{ backgroundColor: value || '#000000' }}
18
18
  />
19
- <span className="text-sm">{value || '-'}</span>
19
+ <span className="text-sm">{value || <EmptyValue />}</span>
20
20
  </div>
21
21
  );
22
22
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -22,7 +22,7 @@ export function CurrencyField({ value, onChange, field, readonly, errorMessage,
22
22
  const precision = currencyField?.precision ?? 2;
23
23
 
24
24
  if (readonly) {
25
- if (value == null) return <span className="text-sm">-</span>;
25
+ if (value == null) return <EmptyValue />;
26
26
  return (
27
27
  <span className="text-sm font-medium tabular-nums">
28
28
  {formatCurrency(Number(value), currency)}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -8,7 +8,7 @@ import { FieldWidgetProps } from './types';
8
8
  */
9
9
  export function DateField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
10
10
  if (readonly) {
11
- return <span className="text-sm">{value ? new Date(value).toLocaleDateString() : '-'}</span>;
11
+ return value ? <span className="text-sm">{new Date(value).toLocaleDateString()}</span> : <EmptyValue />;
12
12
  }
13
13
 
14
14
  // Filter out non-DOM props
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -8,7 +8,7 @@ import { FieldWidgetProps } from './types';
8
8
  */
9
9
  export function DateTimeField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
10
10
  if (readonly) {
11
- if (!value) return <span className="text-sm">-</span>;
11
+ if (!value) return <EmptyValue />;
12
12
  const date = new Date(value);
13
13
  return (
14
14
  <span className="text-sm">
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -9,7 +9,7 @@ import { FieldWidgetProps } from './types';
9
9
  export function EmailField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
10
10
  const config = field || (props as any).schema;
11
11
  if (readonly) {
12
- if (!value) return <span className="text-sm">-</span>;
12
+ if (!value) return <EmptyValue />;
13
13
  return (
14
14
  <a
15
15
  href={`mailto:${value}`}
@@ -1,5 +1,5 @@
1
1
  import React, { useRef, useState, useCallback } from 'react';
2
- import { Button } from '@object-ui/components';
2
+ import { Button, EmptyValue } from '@object-ui/components';
3
3
  import { Upload, X, File as FileIcon, ImageIcon } from 'lucide-react';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
@@ -86,7 +86,7 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
86
86
  }, [accept, processFiles]);
87
87
 
88
88
  if (readonly) {
89
- if (!value) return <span className="text-sm">-</span>;
89
+ if (!value) return <EmptyValue />;
90
90
 
91
91
  const readonlyFiles = Array.isArray(value) ? value : [value];
92
92
  return (
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { EmptyValue } from '@object-ui/components';
2
3
  import { FieldWidgetProps } from './types';
3
4
 
4
5
  /**
@@ -9,18 +10,19 @@ export function FormulaField({ value, field, ...props }: FieldWidgetProps<any>)
9
10
  const formulaField = (field || (props as any).schema) as any;
10
11
  const returnType = formulaField?.return_type || 'text';
11
12
 
12
- // Format based on return type
13
- let displayValue = '-';
14
- if (value != null) {
15
- if (returnType === 'number' || returnType === 'currency') {
16
- displayValue = typeof value === 'number' ? value.toFixed(2) : String(value);
17
- } else if (returnType === 'boolean') {
18
- displayValue = value ? 'Yes' : 'No';
19
- } else if (returnType === 'date') {
20
- displayValue = new Date(value).toLocaleDateString();
21
- } else {
22
- displayValue = String(value);
23
- }
13
+ if (value == null) {
14
+ return <EmptyValue className={props.className} />;
15
+ }
16
+
17
+ let displayValue: string;
18
+ if (returnType === 'number' || returnType === 'currency') {
19
+ displayValue = typeof value === 'number' ? value.toFixed(2) : String(value);
20
+ } else if (returnType === 'boolean') {
21
+ displayValue = value ? 'Yes' : 'No';
22
+ } else if (returnType === 'date') {
23
+ displayValue = new Date(value).toLocaleDateString();
24
+ } else {
25
+ displayValue = String(value);
24
26
  }
25
27
 
26
28
  return (
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input, Button, Label } from '@object-ui/components';
2
+ import { Input, Button, Label, EmptyValue } from '@object-ui/components';
3
3
  import { MapPin, Crosshair } from 'lucide-react';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
@@ -56,7 +56,7 @@ export function GeolocationField({ value, onChange, field, readonly, ...props }:
56
56
  };
57
57
 
58
58
  const formatLocation = (loc: GeolocationValue): string => {
59
- if (!loc.latitude || !loc.longitude) return '-';
59
+ if (!loc.latitude || !loc.longitude) return '';
60
60
  return `${loc.latitude.toFixed(6)}, ${loc.longitude.toFixed(6)}`;
61
61
  };
62
62
 
@@ -67,10 +67,11 @@ export function GeolocationField({ value, onChange, field, readonly, ...props }:
67
67
  };
68
68
 
69
69
  if (readonly) {
70
+ const formatted = formatLocation(location);
70
71
  return (
71
72
  <div className="flex items-center gap-2">
72
73
  <MapPin className="w-4 h-4 text-muted-foreground" />
73
- <span className="text-sm">{formatLocation(location)}</span>
74
+ {formatted ? <span className="text-sm">{formatted}</span> : <EmptyValue />}
74
75
  {location.latitude && location.longitude && (
75
76
  <Button
76
77
  type="button"
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { FieldWidgetProps } from './types';
3
- import { cn } from '@object-ui/components';
3
+ import { cn, EmptyValue } from '@object-ui/components';
4
4
 
5
5
  /**
6
6
  * GridField - Sub-table/grid data display
@@ -12,7 +12,7 @@ export function GridField({ value, field, readonly, ...props }: FieldWidgetProps
12
12
  const columns = gridField?.columns || [];
13
13
 
14
14
  if (!value || !Array.isArray(value)) {
15
- return <span className="text-sm text-muted-foreground">-</span>;
15
+ return <EmptyValue />;
16
16
  }
17
17
 
18
18
  if (readonly) {
@@ -45,7 +45,7 @@ export function GridField({ value, field, readonly, ...props }: FieldWidgetProps
45
45
  <tr key={rowIdx} className="hover:bg-muted/50 transition-colors">
46
46
  {columns.map((col: any, colIdx: number) => (
47
47
  <td key={colIdx} className="px-3 py-2 text-foreground">
48
- {row[col.name] != null ? String(row[col.name]) : '-'}
48
+ {row[col.name] != null ? String(row[col.name]) : <EmptyValue />}
49
49
  </td>
50
50
  ))}
51
51
  </tr>
@@ -1,5 +1,5 @@
1
1
  import React, { useRef } from 'react';
2
- import { Button } from '@object-ui/components';
2
+ import { Button, EmptyValue } from '@object-ui/components';
3
3
  import { Upload, X, Image as ImageIcon } from 'lucide-react';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
@@ -14,7 +14,7 @@ export function ImageField({ value, onChange, field, readonly, ...props }: Field
14
14
  const accept = imageField?.accept ? imageField.accept.join(',') : 'image/*';
15
15
 
16
16
  if (readonly) {
17
- if (!value) return <span className="text-sm">-</span>;
17
+ if (!value) return <EmptyValue />;
18
18
 
19
19
  const images = Array.isArray(value) ? value : [value];
20
20
  return (
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -15,7 +15,7 @@ export function LocationField({ value, onChange, field, readonly, ...props }: Fi
15
15
  : '';
16
16
 
17
17
  if (readonly) {
18
- return <span className="text-sm">{displayValue || '-'}</span>;
18
+ return <span className="text-sm">{displayValue || <EmptyValue />}</span>;
19
19
  }
20
20
 
21
21
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1,12 +1,10 @@
1
1
  import React, { useState, useEffect, useCallback, useRef, useContext, useMemo } from 'react';
2
- import {
3
- Button,
2
+ import { Button,
4
3
  Input,
5
4
  Badge,
6
5
  Popover,
7
6
  PopoverTrigger,
8
- PopoverContent,
9
- } from '@object-ui/components';
7
+ PopoverContent, EmptyValue } from '@object-ui/components';
10
8
  import { Search, X, Loader2, AlertCircle, Plus, TableProperties } from 'lucide-react';
11
9
  import { FieldWidgetProps } from './types';
12
10
  import type { DataSource, QueryParams, LookupColumnDef } from '@object-ui/types';
@@ -337,7 +335,7 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
337
335
 
338
336
  if (readonly) {
339
337
  if (!selectedOptions.length) {
340
- return <span className="text-sm">-</span>;
338
+ return <EmptyValue />;
341
339
  }
342
340
 
343
341
  if (multiple) {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { NumberFieldMetadata } from '@object-ui/types';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
@@ -9,7 +9,7 @@ import { FieldWidgetProps } from './types';
9
9
  */
10
10
  export function NumberField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<number>) {
11
11
  if (readonly) {
12
- return <span className="text-sm">{value ?? '-'}</span>;
12
+ return value == null ? <EmptyValue /> : <span className="text-sm">{value}</span>;
13
13
  }
14
14
 
15
15
  const numberField = (field || (props as any).schema) as NumberFieldMetadata;
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { Textarea, cn } from '@object-ui/components';
2
+ import { Textarea, cn, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -42,7 +42,7 @@ export function ObjectField({ value, onChange, field, readonly, ...props }: Fiel
42
42
  }, [value, jsonString]);
43
43
 
44
44
  if (readonly) {
45
- if (!value) return <span className="text-sm">-</span>;
45
+ if (!value) return <EmptyValue />;
46
46
  return (
47
47
  <pre className={cn("text-xs bg-gray-50 p-2 rounded border border-gray-200 overflow-auto max-h-40", props.className)}>
48
48
  {JSON.stringify(value, null, 2)}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input, Slider } from '@object-ui/components';
2
+ import { Input, Slider, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -12,7 +12,7 @@ export function PercentField({ value, onChange, field, readonly, errorMessage, c
12
12
  const precision = percentField?.precision ?? 2;
13
13
 
14
14
  if (readonly) {
15
- if (value == null) return <span className="text-sm">-</span>;
15
+ if (value == null) return <EmptyValue />;
16
16
  return (
17
17
  <span className="text-sm font-medium tabular-nums">
18
18
  {(value * 100).toFixed(precision)}%
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -9,7 +9,7 @@ import { FieldWidgetProps } from './types';
9
9
  export function PhoneField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
10
10
  const config = field || (props as any).schema;
11
11
  if (readonly) {
12
- if (!value) return <span className="text-sm">-</span>;
12
+ if (!value) return <EmptyValue />;
13
13
  return (
14
14
  <a
15
15
  href={`tel:${value}`}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input, Button } from '@object-ui/components';
2
+ import { Input, Button, EmptyValue } from '@object-ui/components';
3
3
  import { QrCode, Copy } from 'lucide-react';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
@@ -27,7 +27,7 @@ export function QRCodeField({ value, onChange, field, readonly, ...props }: Fiel
27
27
  if (readonly) {
28
28
  return (
29
29
  <div className="flex items-center gap-3">
30
- <span className="text-sm flex-1 truncate">{value || '-'}</span>
30
+ <span className="text-sm flex-1 truncate">{value || <EmptyValue />}</span>
31
31
  {value && (
32
32
  <Button
33
33
  type="button"
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Textarea } from '@object-ui/components';
2
+ import { Textarea, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -13,7 +13,7 @@ export function RichTextField({ value, onChange, field, readonly, errorMessage,
13
13
  <div
14
14
  className="text-sm prose prose-sm max-w-none"
15
15
  >
16
- {value || '-'}
16
+ {value || <EmptyValue />}
17
17
  </div>
18
18
  );
19
19
  }
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
- import {
3
- Select,
4
- SelectContent,
5
- SelectItem,
6
- SelectTrigger,
7
- SelectValue
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ EmptyValue,
8
9
  } from '@object-ui/components';
9
10
  import { SelectFieldMetadata } from '@object-ui/types';
10
11
  import { useFieldTranslation } from './useFieldTranslation';
@@ -21,7 +22,8 @@ export function SelectField({ value, onChange, field, readonly, ...props }: Fiel
21
22
 
22
23
  if (readonly) {
23
24
  const option = options.find((o) => o.value === value);
24
- return <span className="text-sm">{option?.label || value || '-'}</span>;
25
+ const display = option?.label || value;
26
+ return display ? <span className="text-sm">{display}</span> : <EmptyValue />;
25
27
  }
26
28
 
27
29
  return (
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { EmptyValue } from '@object-ui/components';
2
3
  import { FieldWidgetProps } from './types';
3
4
 
4
5
  /**
@@ -9,16 +10,17 @@ export function SummaryField({ value, field, ...props }: FieldWidgetProps<any>)
9
10
  const summaryField = (field || (props as any).schema) as any;
10
11
  const summaryType = summaryField?.summary_type || 'count';
11
12
 
12
- // Format based on aggregation type
13
- let displayValue = '-';
14
- if (value != null) {
15
- if (summaryType === 'count') {
16
- displayValue = String(value);
17
- } else if (['sum', 'avg', 'min', 'max'].includes(summaryType)) {
18
- displayValue = typeof value === 'number' ? value.toFixed(2) : String(value);
19
- } else {
20
- displayValue = String(value);
21
- }
13
+ if (value == null) {
14
+ return <EmptyValue className={props.className} />;
15
+ }
16
+
17
+ let displayValue: string;
18
+ if (summaryType === 'count') {
19
+ displayValue = String(value);
20
+ } else if (['sum', 'avg', 'min', 'max'].includes(summaryType)) {
21
+ displayValue = typeof value === 'number' ? value.toFixed(2) : String(value);
22
+ } else {
23
+ displayValue = String(value);
22
24
  }
23
25
 
24
26
  return (
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Textarea } from '@object-ui/components';
2
+ import { Textarea, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -10,7 +10,7 @@ export function TextAreaField({ value, onChange, field, readonly, errorMessage,
10
10
  if (readonly) {
11
11
  return (
12
12
  <div className="text-sm whitespace-pre-wrap">
13
- {value || '-'}
13
+ {value || <EmptyValue />}
14
14
  </div>
15
15
  );
16
16
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input, Textarea } from '@object-ui/components';
2
+ import { Input, Textarea, EmptyValue } from '@object-ui/components';
3
3
  import { TextareaFieldMetadata } from '@object-ui/types';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
@@ -11,7 +11,7 @@ export function TextField({ value, onChange, field, readonly, ...props }: FieldW
11
11
  const fieldData = field || (props as any).schema;
12
12
 
13
13
  if (readonly) {
14
- return <span className="text-sm">{value || '-'}</span>;
14
+ return <span className="text-sm">{value || <EmptyValue />}</span>;
15
15
  }
16
16
 
17
17
  // Cast for rows property
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -8,7 +8,7 @@ import { FieldWidgetProps } from './types';
8
8
  */
9
9
  export function TimeField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
10
10
  if (readonly) {
11
- return <span className="text-sm">{value || '-'}</span>;
11
+ return <span className="text-sm">{value || <EmptyValue />}</span>;
12
12
  }
13
13
 
14
14
  // Filter out non-DOM props
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Input } from '@object-ui/components';
2
+ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
@@ -9,7 +9,7 @@ import { FieldWidgetProps } from './types';
9
9
  export function UrlField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
10
10
  const config = field || (props as any).schema;
11
11
  if (readonly) {
12
- if (!value) return <span className="text-sm">-</span>;
12
+ if (!value) return <EmptyValue />;
13
13
 
14
14
  // Validate URL to prevent javascript: or data: URLs
15
15
  const isValidUrl = value.startsWith('http://') || value.startsWith('https://');
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Avatar, AvatarFallback, Badge } from '@object-ui/components';
2
+ import { Avatar, AvatarFallback, Badge, EmptyValue } from '@object-ui/components';
3
3
  import { X } from 'lucide-react';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
@@ -12,7 +12,7 @@ export function UserField({ value, onChange, field, readonly, ...props }: FieldW
12
12
  const multiple = userField?.multiple || false;
13
13
 
14
14
  if (readonly) {
15
- if (!value) return <span className="text-sm">-</span>;
15
+ if (!value) return <EmptyValue />;
16
16
 
17
17
  const users = Array.isArray(value) ? value : [value];
18
18
  return (