@object-ui/fields 0.3.0 → 0.5.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 (85) hide show
  1. package/dist/fields/src/complex-widgets.test.d.ts +1 -0
  2. package/dist/fields/src/datetime-widgets.test.d.ts +1 -0
  3. package/dist/fields/src/fields.stories.d.ts +56 -0
  4. package/dist/fields/src/index.d.ts +48 -0
  5. package/dist/fields/src/new-widgets.test.d.ts +8 -0
  6. package/dist/fields/src/remaining-widgets.test.d.ts +1 -0
  7. package/dist/fields/src/standard-widgets.test.d.ts +0 -0
  8. package/dist/fields/src/text-rich-widgets.test.d.ts +1 -0
  9. package/dist/fields/src/visual-widgets.test.d.ts +1 -0
  10. package/dist/fields/src/widgets/AddressField.d.ts +16 -0
  11. package/dist/fields/src/widgets/AutoNumberField.d.ts +6 -0
  12. package/dist/fields/src/widgets/AvatarField.d.ts +6 -0
  13. package/dist/fields/src/widgets/CodeField.d.ts +7 -0
  14. package/dist/fields/src/widgets/ColorField.d.ts +6 -0
  15. package/dist/fields/src/widgets/CurrencyField.d.ts +1 -1
  16. package/dist/fields/src/widgets/DateTimeField.d.ts +2 -0
  17. package/dist/fields/src/widgets/FileField.d.ts +2 -0
  18. package/dist/fields/src/widgets/FormulaField.d.ts +6 -0
  19. package/dist/fields/src/widgets/GeolocationField.d.ts +14 -0
  20. package/dist/fields/src/widgets/GridField.d.ts +7 -0
  21. package/dist/fields/src/widgets/ImageField.d.ts +2 -0
  22. package/dist/fields/src/widgets/LocationField.d.ts +2 -0
  23. package/dist/fields/src/widgets/LookupField.d.ts +1 -1
  24. package/dist/fields/src/widgets/MasterDetailField.d.ts +14 -0
  25. package/dist/fields/src/widgets/ObjectField.d.ts +6 -0
  26. package/dist/fields/src/widgets/PasswordField.d.ts +2 -0
  27. package/dist/fields/src/widgets/PercentField.d.ts +2 -0
  28. package/dist/fields/src/widgets/QRCodeField.d.ts +6 -0
  29. package/dist/fields/src/widgets/RatingField.d.ts +6 -0
  30. package/dist/fields/src/widgets/SignatureField.d.ts +6 -0
  31. package/dist/fields/src/widgets/SliderField.d.ts +6 -0
  32. package/dist/fields/src/widgets/SummaryField.d.ts +6 -0
  33. package/dist/fields/src/widgets/TimeField.d.ts +2 -0
  34. package/dist/fields/src/widgets/UserField.d.ts +6 -0
  35. package/dist/fields/src/widgets/VectorField.d.ts +6 -0
  36. package/dist/fields/src/widgets/types.d.ts +2 -0
  37. package/dist/index.cjs +4 -2
  38. package/dist/index.js +2003 -618
  39. package/package.json +10 -8
  40. package/src/complex-widgets.test.tsx +146 -0
  41. package/src/datetime-widgets.test.tsx +93 -0
  42. package/src/fields.stories.tsx +566 -0
  43. package/src/index.tsx +303 -68
  44. package/src/new-widgets.test.tsx +302 -0
  45. package/src/remaining-widgets.test.tsx +172 -0
  46. package/src/standard-widgets.test.tsx +278 -0
  47. package/src/text-rich-widgets.test.tsx +104 -0
  48. package/src/visual-widgets.test.tsx +97 -0
  49. package/src/widgets/AddressField.tsx +112 -0
  50. package/src/widgets/AutoNumberField.tsx +16 -0
  51. package/src/widgets/AvatarField.tsx +110 -0
  52. package/src/widgets/BooleanField.tsx +19 -11
  53. package/src/widgets/CodeField.tsx +34 -0
  54. package/src/widgets/ColorField.tsx +44 -0
  55. package/src/widgets/CurrencyField.tsx +8 -7
  56. package/src/widgets/DateField.tsx +5 -2
  57. package/src/widgets/DateTimeField.tsx +28 -0
  58. package/src/widgets/EmailField.tsx +7 -3
  59. package/src/widgets/FileField.tsx +114 -0
  60. package/src/widgets/FormulaField.tsx +31 -0
  61. package/src/widgets/GeolocationField.tsx +151 -0
  62. package/src/widgets/GridField.tsx +63 -0
  63. package/src/widgets/ImageField.tsx +108 -0
  64. package/src/widgets/LocationField.tsx +46 -0
  65. package/src/widgets/LookupField.tsx +5 -5
  66. package/src/widgets/MasterDetailField.tsx +131 -0
  67. package/src/widgets/NumberField.tsx +8 -4
  68. package/src/widgets/ObjectField.tsx +85 -0
  69. package/src/widgets/PasswordField.tsx +45 -0
  70. package/src/widgets/PercentField.tsx +49 -0
  71. package/src/widgets/PhoneField.tsx +7 -3
  72. package/src/widgets/QRCodeField.tsx +96 -0
  73. package/src/widgets/RatingField.tsx +66 -0
  74. package/src/widgets/RichTextField.tsx +5 -5
  75. package/src/widgets/SelectField.tsx +7 -5
  76. package/src/widgets/SignatureField.tsx +140 -0
  77. package/src/widgets/SliderField.tsx +39 -0
  78. package/src/widgets/SummaryField.tsx +29 -0
  79. package/src/widgets/TextAreaField.tsx +9 -6
  80. package/src/widgets/TextField.tsx +13 -8
  81. package/src/widgets/TimeField.tsx +22 -0
  82. package/src/widgets/UrlField.tsx +7 -3
  83. package/src/widgets/UserField.tsx +88 -0
  84. package/src/widgets/VectorField.tsx +28 -0
  85. package/src/widgets/types.ts +2 -0
@@ -0,0 +1,278 @@
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 { describe, it, expect, vi } from 'vitest';
10
+ import React from 'react';
11
+ import { render, screen, fireEvent } from '@testing-library/react';
12
+ import '@testing-library/jest-dom';
13
+ import { CurrencyField } from './widgets/CurrencyField';
14
+ import { NumberField } from './widgets/NumberField';
15
+ import { TextField } from './widgets/TextField';
16
+ import { SelectField } from './widgets/SelectField';
17
+ import { BooleanField } from './widgets/BooleanField';
18
+ import { DateField } from './widgets/DateField';
19
+ import { EmailField } from './widgets/EmailField';
20
+ import { UrlField } from './widgets/UrlField';
21
+ import { PhoneField } from './widgets/PhoneField';
22
+ import { TextAreaField } from './widgets/TextAreaField';
23
+
24
+ const mockOnChange = vi.fn();
25
+
26
+ describe('Standard Field Widgets', () => {
27
+ describe('CurrencyField', () => {
28
+ const fieldMock = { type: 'currency', currency: 'USD', precision: 2 };
29
+
30
+ it('should render value with currency symbol in input mode', () => {
31
+ render(
32
+ <CurrencyField
33
+ value={123.45}
34
+ onChange={mockOnChange}
35
+ field={fieldMock as any}
36
+ />
37
+ );
38
+ const input = screen.getByRole('spinbutton'); // input type="number"
39
+ expect(input).toHaveValue(123.45);
40
+ expect(screen.getByText('$')).toBeInTheDocument();
41
+ });
42
+
43
+ it('should format currency in readonly mode', () => {
44
+ const { container } = render(
45
+ <CurrencyField
46
+ value={1234.56}
47
+ onChange={mockOnChange}
48
+ field={fieldMock as any}
49
+ readonly={true}
50
+ />
51
+ );
52
+ // Expected output format might depend on locale, matching generic USD format
53
+ expect(container.textContent).toMatch(/\$1,234.56/);
54
+ });
55
+
56
+ it('should handle EUR currency', () => {
57
+ render(
58
+ <CurrencyField
59
+ value={100}
60
+ onChange={mockOnChange}
61
+ field={{ ...fieldMock, currency: 'EUR' } as any}
62
+ />
63
+ );
64
+ expect(screen.getByText('EUR')).toBeInTheDocument();
65
+ });
66
+ });
67
+
68
+ describe('NumberField', () => {
69
+ const fieldMock = { type: 'number', precision: 2 };
70
+
71
+ it('should render numeric input', () => {
72
+ render(
73
+ <NumberField
74
+ value={42}
75
+ onChange={mockOnChange}
76
+ field={fieldMock as any}
77
+ />
78
+ );
79
+ const input = screen.getByRole('spinbutton');
80
+ expect(input).toHaveValue(42);
81
+ });
82
+
83
+ it('should call onChange with number', () => {
84
+ render(
85
+ <NumberField
86
+ value={1}
87
+ onChange={mockOnChange}
88
+ field={fieldMock as any}
89
+ />
90
+ );
91
+ const input = screen.getByRole('spinbutton');
92
+ fireEvent.change(input, { target: { value: '10' } });
93
+ expect(mockOnChange).toHaveBeenCalledWith(10);
94
+ });
95
+
96
+ it('should display value in readonly mode', () => {
97
+ const { container } = render(
98
+ <NumberField
99
+ value={99.9}
100
+ onChange={mockOnChange}
101
+ field={fieldMock as any}
102
+ readonly={true}
103
+ />
104
+ );
105
+ expect(container.textContent).toBe('99.9');
106
+ });
107
+ });
108
+
109
+ describe('TextField', () => {
110
+ const fieldMock = { type: 'text' };
111
+
112
+ it('should render text input', () => {
113
+ render(
114
+ <TextField
115
+ value="Hello"
116
+ onChange={mockOnChange}
117
+ field={fieldMock as any}
118
+ />
119
+ );
120
+ const input = screen.getByRole('textbox');
121
+ expect(input).toHaveValue('Hello');
122
+ });
123
+
124
+ it('should render as password when type is password', () => {
125
+ render(
126
+ <TextField
127
+ value="secret"
128
+ onChange={mockOnChange}
129
+ field={{ type: 'password' } as any}
130
+ />
131
+ );
132
+ // getByRole('textbox') doesn't work for password type usually
133
+ const input = screen.getByDisplayValue('secret');
134
+ expect(input).toHaveAttribute('type', 'password');
135
+ });
136
+
137
+ it('should display value in readonly mode', () => {
138
+ const { container } = render(
139
+ <TextField
140
+ value="ReadOnly"
141
+ onChange={mockOnChange}
142
+ field={fieldMock as any}
143
+ readonly={true}
144
+ />
145
+ );
146
+ expect(container.textContent).toBe('ReadOnly');
147
+ });
148
+ });
149
+
150
+ describe('SelectField', () => {
151
+ const fieldMock = {
152
+ type: 'select',
153
+ options: [
154
+ { label: 'Option A', value: 'a' },
155
+ { label: 'Option B', value: 'b' }
156
+ ]
157
+ };
158
+
159
+ it('should render select trigger (Combobox/Select)', () => {
160
+ render(
161
+ <SelectField
162
+ value={''}
163
+ onChange={mockOnChange}
164
+ field={fieldMock as any}
165
+ />
166
+ );
167
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
168
+ });
169
+
170
+ it('should display label in readonly mode', () => {
171
+ const { container } = render(
172
+ <SelectField
173
+ value="a"
174
+ onChange={mockOnChange}
175
+ field={fieldMock as any}
176
+ readonly={true}
177
+ />
178
+ );
179
+ expect(container.textContent).toBe('Option A');
180
+ });
181
+ });
182
+
183
+ describe('BooleanField', () => {
184
+ const fieldMock = { type: 'boolean' };
185
+
186
+ it('should render checkbox/switch', () => {
187
+ render(
188
+ <BooleanField
189
+ value={true}
190
+ onChange={mockOnChange}
191
+ field={fieldMock as any}
192
+ />
193
+ );
194
+ // Ensure we can find the switch role (Shadcn switch uses role="switch")
195
+ expect(screen.getByRole('switch')).toBeChecked();
196
+ });
197
+
198
+ it('should toggle value', () => {
199
+ render(
200
+ <BooleanField
201
+ value={false}
202
+ onChange={mockOnChange}
203
+ field={fieldMock as any}
204
+ />
205
+ );
206
+ const toggle = screen.getByRole('switch');
207
+ fireEvent.click(toggle);
208
+ expect(mockOnChange).toHaveBeenCalledWith(true);
209
+ });
210
+
211
+ it('should display Yes/No in readonly mode', () => {
212
+ const { container } = render(
213
+ <BooleanField
214
+ value={true}
215
+ onChange={mockOnChange}
216
+ field={fieldMock as any}
217
+ readonly={true}
218
+ />
219
+ );
220
+ expect(container.textContent).toBe('Yes');
221
+ });
222
+ });
223
+
224
+ describe('EmailField', () => {
225
+ it('should render email input', () => {
226
+ render(
227
+ <EmailField
228
+ value="test@example.com"
229
+ onChange={mockOnChange}
230
+ field={{ type: 'email' } as any}
231
+ />
232
+ );
233
+ const input = screen.getByDisplayValue('test@example.com');
234
+ expect(input).toHaveAttribute('type', 'email');
235
+ });
236
+
237
+ it('should render as link in readonly mode', () => {
238
+ render(
239
+ <EmailField
240
+ value="test@example.com"
241
+ onChange={mockOnChange}
242
+ field={{ type: 'email' } as any}
243
+ readonly={true}
244
+ />
245
+ );
246
+ const link = screen.getByRole('link');
247
+ expect(link).toHaveAttribute('href', 'mailto:test@example.com');
248
+ expect(link).toHaveTextContent('test@example.com');
249
+ });
250
+ });
251
+
252
+ describe('UrlField', () => {
253
+ it('should render url input', () => {
254
+ render(
255
+ <UrlField
256
+ value="https://example.com"
257
+ onChange={mockOnChange}
258
+ field={{ type: 'url' } as any}
259
+ />
260
+ );
261
+ const input = screen.getByDisplayValue('https://example.com');
262
+ expect(input).toHaveAttribute('type', 'url');
263
+ });
264
+
265
+ it('should render clickable link in readonly mode', () => {
266
+ render(
267
+ <UrlField
268
+ value="https://example.com"
269
+ onChange={mockOnChange}
270
+ field={{ type: 'url' } as any}
271
+ readonly={true}
272
+ />
273
+ );
274
+ const link = screen.getByRole('link');
275
+ expect(link).toHaveAttribute('href', 'https://example.com');
276
+ });
277
+ });
278
+ });
@@ -0,0 +1,104 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { TextAreaField } from './widgets/TextAreaField';
4
+ import { RichTextField } from './widgets/RichTextField';
5
+ import { CodeField } from './widgets/CodeField';
6
+ import { PasswordField } from './widgets/PasswordField';
7
+ import type { FieldWidgetProps } from './widgets/types';
8
+
9
+ const mockField = {
10
+ name: 'test_field',
11
+ label: 'Test Field',
12
+ } as any;
13
+
14
+ const baseProps: FieldWidgetProps<string> = {
15
+ field: mockField,
16
+ value: '',
17
+ onChange: vi.fn(),
18
+ readonly: false,
19
+ };
20
+
21
+ describe('Text & Rich Content Widgets', () => {
22
+ describe('TextAreaField', () => {
23
+ it('renders textarea in edit mode', () => {
24
+ render(<TextAreaField {...baseProps} />);
25
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
26
+ });
27
+
28
+ it('handles max length indicator', () => {
29
+ const propsWithMax = {
30
+ ...baseProps,
31
+ field: { ...mockField, max_length: 100 }
32
+ };
33
+ render(<TextAreaField {...propsWithMax} value="testing" />);
34
+ expect(screen.getByLabelText('Character count: 7 of 100')).toBeInTheDocument();
35
+ });
36
+
37
+ it('renders whitespace-pre-wrap in readonly mode', () => {
38
+ const text = 'Line 1\nLine 2';
39
+ render(<TextAreaField {...baseProps} readonly value={text} />);
40
+ // Check existence logic
41
+ const display = screen.getByText((content) =>
42
+ content.includes('Line 1') && content.includes('Line 2')
43
+ );
44
+ expect(display).toHaveClass('whitespace-pre-wrap');
45
+ });
46
+ });
47
+
48
+ describe('RichTextField', () => {
49
+ // Current implementation is a simpler textarea wrapper
50
+ it('renders textarea with format indicator', () => {
51
+ render(<RichTextField {...baseProps} />);
52
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
53
+ expect(screen.getByText((content) => content.includes('Format: markdown'))).toBeInTheDocument();
54
+ });
55
+
56
+ it('renders prose in readonly mode', () => {
57
+ render(<RichTextField {...baseProps} readonly value="# Heading" />);
58
+ const display = screen.getByText('# Heading');
59
+ expect(display).toHaveClass('prose');
60
+ });
61
+ });
62
+
63
+ describe('CodeField', () => {
64
+ it('renders textarea with monospace font', () => {
65
+ render(<CodeField {...baseProps} />);
66
+ const input = screen.getByRole('textbox');
67
+ expect(input).toHaveClass('font-mono');
68
+ });
69
+
70
+ it('renders code block in readonly mode', () => {
71
+ render(<CodeField {...baseProps} readonly value="const x = 1;" />);
72
+ // Check for pre > code structure
73
+ const pre = document.querySelector('pre');
74
+ expect(pre).toBeInTheDocument();
75
+ expect(pre).toHaveTextContent('const x = 1;');
76
+ });
77
+ });
78
+
79
+ describe('PasswordField', () => {
80
+ it('renders password input by default', () => {
81
+ const { container } = render(<PasswordField {...baseProps} />);
82
+ const input = container.querySelector('input[type="password"]');
83
+ expect(input).toBeInTheDocument();
84
+ });
85
+
86
+ it('toggles visibility', () => {
87
+ const { container } = render(<PasswordField {...baseProps} />);
88
+ const button = screen.getByRole('button'); // The eye icon button
89
+ const input = container.querySelector('input') as HTMLInputElement;
90
+
91
+ expect(input.type).toBe('password');
92
+ fireEvent.click(button);
93
+ expect(input.type).toBe('text');
94
+ fireEvent.click(button);
95
+ expect(input.type).toBe('password');
96
+ });
97
+
98
+ it('renders masked bullets in readonly mode', () => {
99
+ render(<PasswordField {...baseProps} readonly value="secret" />);
100
+ expect(screen.getByText('••••••••')).toBeInTheDocument();
101
+ expect(screen.queryByText('secret')).not.toBeInTheDocument();
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,97 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { ColorField } from './widgets/ColorField';
4
+ import { RatingField } from './widgets/RatingField';
5
+ import { SliderField } from './widgets/SliderField';
6
+ import { QRCodeField } from './widgets/QRCodeField'; // Intentionally simpler than Image/Avatar
7
+ import type { FieldWidgetProps } from './widgets/types';
8
+
9
+ const mockField = {
10
+ name: 'test_field',
11
+ label: 'Test Field',
12
+ } as any;
13
+
14
+ const baseProps: FieldWidgetProps<any> = {
15
+ field: mockField,
16
+ value: undefined,
17
+ onChange: vi.fn(),
18
+ readonly: false,
19
+ };
20
+
21
+ describe('Visual & Display Widgets', () => {
22
+ describe('ColorField', () => {
23
+ it('renders color picker + text input in edit mode', () => {
24
+ const { container } = render(<ColorField {...baseProps} value="#00ff00" />);
25
+ const colorInput = container.querySelector('input[type="color"]');
26
+ const textInput = screen.getByRole('textbox');
27
+
28
+ expect(colorInput).toBeInTheDocument();
29
+ expect(textInput).toBeInTheDocument();
30
+ expect(textInput).toHaveValue('#00ff00');
31
+ });
32
+
33
+ it('syncs change from color picker', () => {
34
+ const { container } = render(<ColorField {...baseProps} />);
35
+ const colorInput = container.querySelector('input[type="color"]') as HTMLInputElement;
36
+ fireEvent.change(colorInput, { target: { value: '#ff0000' } });
37
+ expect(baseProps.onChange).toHaveBeenCalledWith('#ff0000');
38
+ });
39
+
40
+ it('shows preview box in readonly mode', () => {
41
+ const { container } = render(<ColorField {...baseProps} readonly value="#0000ff" />);
42
+ // Check for the style attribute on the div
43
+ // We search for a div that has the style background-color
44
+ // This is a bit fragile but standard for inline styles
45
+ const preview = container.querySelector('div[style="background-color: #0000ff;"]');
46
+ // Or looser check:
47
+ const divs = container.querySelectorAll('div');
48
+ const found = Array.from(divs).some(d => d.style.backgroundColor === 'rgb(0, 0, 255)' || d.style.backgroundColor === '#0000ff');
49
+ expect(found).toBe(true);
50
+ expect(screen.getByText('#0000ff')).toBeInTheDocument();
51
+ });
52
+ });
53
+
54
+ describe('RatingField', () => {
55
+ it('renders stars', () => {
56
+ render(<RatingField {...baseProps} value={3} />);
57
+ // Should see 5 buttons (default max)
58
+ const buttons = screen.getAllByRole('button');
59
+ expect(buttons).toHaveLength(5);
60
+ });
61
+
62
+ it('highlights correct number of stars in readonly', () => {
63
+ const { container } = render(<RatingField {...baseProps} readonly value={3} />);
64
+ // Should have 3 filled stars, 2 empty.
65
+ // Logic: fill-yellow-400
66
+ const filled = container.querySelectorAll('.fill-yellow-400');
67
+ expect(filled).toHaveLength(3);
68
+ expect(screen.getByText('3 / 5')).toBeInTheDocument();
69
+ });
70
+
71
+ it('calls onChange when clicking star', () => {
72
+ render(<RatingField {...baseProps} />);
73
+ const buttons = screen.getAllByRole('button');
74
+ fireEvent.click(buttons[3]); // 4th star (index 3) -> value 4
75
+ expect(baseProps.onChange).toHaveBeenCalledWith(4);
76
+ });
77
+ });
78
+
79
+ describe('SliderField', () => {
80
+ it('renders slider', () => {
81
+ const { container } = render(<SliderField {...baseProps} value={50} />);
82
+ // Radix slider usually has role="slider"
83
+ const slider = screen.getByRole('slider');
84
+ expect(slider).toBeInTheDocument();
85
+ // Check text display
86
+ expect(screen.getByText('50')).toBeInTheDocument();
87
+ });
88
+
89
+ it('renders readonly view', () => {
90
+ render(<SliderField {...baseProps} readonly value={75} />);
91
+ expect(screen.getByText('75')).toBeInTheDocument();
92
+ expect(screen.getByText('/ 100')).toBeInTheDocument();
93
+ expect(screen.queryByRole('slider')).not.toBeInTheDocument();
94
+ });
95
+ });
96
+
97
+ });
@@ -0,0 +1,112 @@
1
+ import React from 'react';
2
+ import { Input, Label } from '@object-ui/components';
3
+ import { FieldWidgetProps } from './types';
4
+
5
+ /**
6
+ * Address data structure
7
+ */
8
+ export interface AddressValue {
9
+ street?: string;
10
+ city?: string;
11
+ state?: string;
12
+ zipCode?: string;
13
+ country?: string;
14
+ }
15
+
16
+ /**
17
+ * Address field widget - provides a structured address input
18
+ * Supports street, city, state, zip code, and country
19
+ */
20
+ export function AddressField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<AddressValue>) {
21
+ const address = value || {};
22
+
23
+ const handleFieldChange = (fieldName: keyof AddressValue, fieldValue: string) => {
24
+ onChange({
25
+ ...address,
26
+ [fieldName]: fieldValue,
27
+ });
28
+ };
29
+
30
+ const formatAddress = (addr: AddressValue): string => {
31
+ const parts = [
32
+ addr.street,
33
+ addr.city,
34
+ [addr.state, addr.zipCode].filter(Boolean).join(' '),
35
+ addr.country,
36
+ ].filter(Boolean);
37
+ return parts.join(', ') || '-';
38
+ };
39
+
40
+ if (readonly) {
41
+ return <span className="text-sm">{formatAddress(address)}</span>;
42
+ }
43
+
44
+ return (
45
+ <div className="space-y-3">
46
+ <div>
47
+ <Label htmlFor="street" className="text-xs">Street Address</Label>
48
+ <Input
49
+ id="street"
50
+ type="text"
51
+ value={address.street || ''}
52
+ onChange={(e) => handleFieldChange('street', e.target.value)}
53
+ placeholder="123 Main St"
54
+ disabled={readonly || props.disabled}
55
+ className={props.className}
56
+ />
57
+ </div>
58
+
59
+ <div className="grid grid-cols-2 gap-3">
60
+ <div>
61
+ <Label htmlFor="city" className="text-xs">City</Label>
62
+ <Input
63
+ id="city"
64
+ type="text"
65
+ value={address.city || ''}
66
+ onChange={(e) => handleFieldChange('city', e.target.value)}
67
+ placeholder="San Francisco"
68
+ disabled={readonly || props.disabled}
69
+ />
70
+ </div>
71
+
72
+ <div>
73
+ <Label htmlFor="state" className="text-xs">State / Province</Label>
74
+ <Input
75
+ id="state"
76
+ type="text"
77
+ value={address.state || ''}
78
+ onChange={(e) => handleFieldChange('state', e.target.value)}
79
+ placeholder="CA"
80
+ disabled={readonly || props.disabled}
81
+ />
82
+ </div>
83
+ </div>
84
+
85
+ <div className="grid grid-cols-2 gap-3">
86
+ <div>
87
+ <Label htmlFor="zipCode" className="text-xs">ZIP / Postal Code</Label>
88
+ <Input
89
+ id="zipCode"
90
+ type="text"
91
+ value={address.zipCode || ''}
92
+ onChange={(e) => handleFieldChange('zipCode', e.target.value)}
93
+ placeholder="94102"
94
+ disabled={readonly || props.disabled}
95
+ />
96
+ </div>
97
+
98
+ <div>
99
+ <Label htmlFor="country" className="text-xs">Country</Label>
100
+ <Input
101
+ id="country"
102
+ type="text"
103
+ value={address.country || ''}
104
+ onChange={(e) => handleFieldChange('country', e.target.value)}
105
+ placeholder="United States"
106
+ disabled={readonly || props.disabled}
107
+ />
108
+ </div>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { FieldWidgetProps } from './types';
3
+
4
+ /**
5
+ * AutoNumberField - Read-only auto-generated field
6
+ * Values are automatically generated by the system and cannot be edited
7
+ */
8
+ export function AutoNumberField({ value, field, ...props }: FieldWidgetProps<string | number>) {
9
+ const displayValue = value != null ? String(value) : '-';
10
+
11
+ return (
12
+ <span className={`text-sm font-mono text-gray-700 ${props.className || ''}`}>
13
+ {displayValue}
14
+ </span>
15
+ );
16
+ }