@object-ui/fields 0.5.0 → 3.0.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 (48) hide show
  1. package/dist/fields/src/__tests__/datetime-timezone.test.d.ts +0 -0
  2. package/dist/fields/src/__tests__/extreme-inputs.test.d.ts +0 -0
  3. package/dist/fields/src/__tests__/form-variants.test.d.ts +0 -0
  4. package/dist/fields/src/__tests__/validation-feedback.test.d.ts +0 -0
  5. package/dist/fields/src/fields-edge-cases.stories.d.ts +45 -0
  6. package/dist/fields/src/index.d.ts +2 -1
  7. package/dist/fields/src/widgets/BooleanField.d.ts +4 -0
  8. package/dist/fields/src/widgets/DateField.d.ts +4 -0
  9. package/dist/fields/src/widgets/DateTimeField.d.ts +4 -0
  10. package/dist/fields/src/widgets/EmailField.d.ts +4 -0
  11. package/dist/fields/src/widgets/FileField.d.ts +4 -0
  12. package/dist/fields/src/widgets/ImageField.d.ts +4 -0
  13. package/dist/fields/src/widgets/LocationField.d.ts +4 -0
  14. package/dist/fields/src/widgets/NumberField.d.ts +4 -0
  15. package/dist/fields/src/widgets/PasswordField.d.ts +4 -0
  16. package/dist/fields/src/widgets/PercentField.d.ts +4 -0
  17. package/dist/fields/src/widgets/PhoneField.d.ts +4 -0
  18. package/dist/fields/src/widgets/SelectField.d.ts +4 -0
  19. package/dist/fields/src/widgets/TextAreaField.d.ts +4 -0
  20. package/dist/fields/src/widgets/TextField.d.ts +4 -0
  21. package/dist/fields/src/widgets/TimeField.d.ts +4 -0
  22. package/dist/fields/src/widgets/UrlField.d.ts +4 -0
  23. package/dist/index.cjs +3 -3
  24. package/dist/index.js +216 -197
  25. package/package.json +9 -9
  26. package/src/__tests__/datetime-timezone.test.tsx +770 -0
  27. package/src/__tests__/extreme-inputs.test.tsx +567 -0
  28. package/src/__tests__/form-variants.test.tsx +306 -0
  29. package/src/__tests__/validation-feedback.test.tsx +352 -0
  30. package/src/fields-edge-cases.stories.tsx +407 -0
  31. package/src/index.tsx +36 -11
  32. package/src/widgets/BooleanField.tsx +4 -0
  33. package/src/widgets/DateField.tsx +4 -0
  34. package/src/widgets/DateTimeField.tsx +4 -0
  35. package/src/widgets/EmailField.tsx +4 -0
  36. package/src/widgets/FileField.tsx +4 -0
  37. package/src/widgets/ImageField.tsx +4 -0
  38. package/src/widgets/LocationField.tsx +4 -0
  39. package/src/widgets/NumberField.tsx +4 -0
  40. package/src/widgets/PasswordField.tsx +4 -0
  41. package/src/widgets/PercentField.tsx +4 -0
  42. package/src/widgets/PhoneField.tsx +4 -0
  43. package/src/widgets/SelectField.tsx +4 -0
  44. package/src/widgets/SignatureField.tsx +1 -1
  45. package/src/widgets/TextAreaField.tsx +4 -0
  46. package/src/widgets/TextField.tsx +4 -0
  47. package/src/widgets/TimeField.tsx +4 -0
  48. package/src/widgets/UrlField.tsx +4 -0
@@ -0,0 +1,407 @@
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 type { Meta, StoryObj } from '@storybook/react';
10
+ import React, { useState } from 'react';
11
+ import {
12
+ TextField,
13
+ NumberField,
14
+ SelectField,
15
+ CurrencyField,
16
+ EmailField,
17
+ TextAreaField,
18
+ BooleanField,
19
+ UrlField,
20
+ } from './index';
21
+ import { FieldMetadata } from '@object-ui/types';
22
+
23
+ /**
24
+ * **Field Widgets – Edge Cases** from `@object-ui/fields`.
25
+ *
26
+ * Stories that exercise boundary conditions: empty values, overflow text,
27
+ * RTL content, emoji-only input, error states, disabled state, and readonly
28
+ * variants across multiple field types.
29
+ */
30
+ const meta = {
31
+ title: 'Fields/Edge Cases',
32
+ parameters: {
33
+ layout: 'centered',
34
+ },
35
+ tags: ['autodocs'],
36
+ argTypes: {
37
+ readonly: { control: 'boolean', description: 'Toggle readonly mode' },
38
+ disabled: { control: 'boolean', description: 'Toggle disabled mode' },
39
+ },
40
+ } satisfies Meta;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<any>;
44
+
45
+ // Wrapper to handle state since these are controlled components
46
+ const FieldWrapper = ({
47
+ Component,
48
+ field,
49
+ initialValue,
50
+ readonly = false,
51
+ disabled = false,
52
+ errorMessage,
53
+ }: {
54
+ Component: any;
55
+ field: Partial<FieldMetadata>;
56
+ initialValue?: any;
57
+ readonly?: boolean;
58
+ disabled?: boolean;
59
+ errorMessage?: string;
60
+ }) => {
61
+ const [value, setValue] = useState(initialValue);
62
+
63
+ const fullField = {
64
+ name: 'test_field',
65
+ label: 'Test Field',
66
+ type: 'text',
67
+ ...field,
68
+ } as FieldMetadata;
69
+
70
+ return (
71
+ <div className="w-[350px] space-y-2 p-4 border rounded-lg shadow-sm bg-white">
72
+ <div className="flex justify-between items-center mb-2">
73
+ <label className="text-sm font-semibold text-gray-700">{fullField.label}</label>
74
+ {readonly && (
75
+ <span className="text-[10px] bg-slate-100 px-2 py-0.5 rounded text-slate-500 font-medium">
76
+ Read Only
77
+ </span>
78
+ )}
79
+ {disabled && (
80
+ <span className="text-[10px] bg-slate-100 px-2 py-0.5 rounded text-slate-500 font-medium">
81
+ Disabled
82
+ </span>
83
+ )}
84
+ </div>
85
+
86
+ <Component
87
+ value={value}
88
+ onChange={(val: any) => {
89
+ console.log('onChange', val);
90
+ setValue(val);
91
+ }}
92
+ field={fullField}
93
+ readonly={readonly}
94
+ disabled={disabled}
95
+ errorMessage={errorMessage}
96
+ />
97
+
98
+ {errorMessage && (
99
+ <div className="text-xs text-destructive mt-1">{errorMessage}</div>
100
+ )}
101
+
102
+ {!readonly && (
103
+ <div className="text-xs text-muted-foreground mt-4 pt-2 border-t font-mono overflow-hidden text-ellipsis">
104
+ Value: {JSON.stringify(value)}
105
+ </div>
106
+ )}
107
+ </div>
108
+ );
109
+ };
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // 1. Empty / Null / Undefined Values
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export const EmptyText: Story = {
116
+ name: 'Empty – Text (undefined)',
117
+ render: () => (
118
+ <FieldWrapper
119
+ Component={TextField}
120
+ field={{ label: 'Empty Text', type: 'text', placeholder: 'Type here…' }}
121
+ initialValue={undefined}
122
+ />
123
+ ),
124
+ };
125
+
126
+ export const NullNumber: Story = {
127
+ name: 'Empty – Number (null)',
128
+ render: () => (
129
+ <FieldWrapper
130
+ Component={NumberField}
131
+ field={{ label: 'Null Number', type: 'number', precision: 2 }}
132
+ initialValue={null}
133
+ />
134
+ ),
135
+ };
136
+
137
+ export const EmptySelect: Story = {
138
+ name: 'Empty – Select (empty string)',
139
+ render: () => (
140
+ <FieldWrapper
141
+ Component={SelectField}
142
+ field={{
143
+ label: 'No Selection',
144
+ type: 'select',
145
+ options: [
146
+ { label: 'Option A', value: 'a' },
147
+ { label: 'Option B', value: 'b' },
148
+ ],
149
+ }}
150
+ initialValue=""
151
+ />
152
+ ),
153
+ };
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // 2. Very Long Strings (overflow handling)
157
+ // ---------------------------------------------------------------------------
158
+
159
+ const LONG_STRING =
160
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20).trim();
161
+
162
+ export const LongText: Story = {
163
+ name: 'Overflow – Long Text',
164
+ render: () => (
165
+ <FieldWrapper
166
+ Component={TextField}
167
+ field={{ label: 'Long Value', type: 'text' }}
168
+ initialValue={LONG_STRING}
169
+ />
170
+ ),
171
+ };
172
+
173
+ export const LongTextArea: Story = {
174
+ name: 'Overflow – Long TextArea',
175
+ render: () => (
176
+ <FieldWrapper
177
+ Component={TextAreaField}
178
+ field={{ label: 'Long TextArea', type: 'textarea', rows: 4 }}
179
+ initialValue={LONG_STRING}
180
+ />
181
+ ),
182
+ };
183
+
184
+ export const LongUrl: Story = {
185
+ name: 'Overflow – Long URL',
186
+ render: () => (
187
+ <FieldWrapper
188
+ Component={UrlField}
189
+ field={{ label: 'Long URL', type: 'url' }}
190
+ initialValue={`https://example.com/${'segment/'.repeat(50)}`}
191
+ />
192
+ ),
193
+ };
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // 3. RTL Text (Arabic)
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export const RTLText: Story = {
200
+ name: 'RTL – Arabic Text',
201
+ render: () => (
202
+ <FieldWrapper
203
+ Component={TextField}
204
+ field={{ label: 'Arabic Input', type: 'text' }}
205
+ initialValue="مرحبا بالعالم — هذا نص عربي طويل لاختبار الاتجاه"
206
+ />
207
+ ),
208
+ };
209
+
210
+ export const RTLTextArea: Story = {
211
+ name: 'RTL – Arabic TextArea',
212
+ render: () => (
213
+ <FieldWrapper
214
+ Component={TextAreaField}
215
+ field={{ label: 'Arabic TextArea', type: 'textarea', rows: 3 }}
216
+ initialValue="هذا نص عربي يمتد على عدة أسطر.\nالسطر الثاني هنا.\nالسطر الثالث."
217
+ />
218
+ ),
219
+ };
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // 4. Emoji-Only Values
223
+ // ---------------------------------------------------------------------------
224
+
225
+ export const EmojiText: Story = {
226
+ name: 'Emoji – Text Field',
227
+ render: () => (
228
+ <FieldWrapper
229
+ Component={TextField}
230
+ field={{ label: 'Emoji Only', type: 'text' }}
231
+ initialValue="🚀🔥✨💯🎉🌍"
232
+ />
233
+ ),
234
+ };
235
+
236
+ export const EmojiEmail: Story = {
237
+ name: 'Emoji – Email Field (invalid)',
238
+ render: () => (
239
+ <FieldWrapper
240
+ Component={EmailField}
241
+ field={{ label: 'Emoji Email', type: 'email' }}
242
+ initialValue="🚀@emoji.com"
243
+ />
244
+ ),
245
+ };
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // 5. Error State (with errorMessage prop)
249
+ // ---------------------------------------------------------------------------
250
+
251
+ export const ErrorText: Story = {
252
+ name: 'Error – Text Field',
253
+ render: () => (
254
+ <FieldWrapper
255
+ Component={TextField}
256
+ field={{ label: 'Name', type: 'text', placeholder: 'Required field' }}
257
+ initialValue=""
258
+ errorMessage="This field is required."
259
+ />
260
+ ),
261
+ };
262
+
263
+ export const ErrorNumber: Story = {
264
+ name: 'Error – Number Field',
265
+ render: () => (
266
+ <FieldWrapper
267
+ Component={NumberField}
268
+ field={{ label: 'Age', type: 'number', precision: 0 }}
269
+ initialValue={-5}
270
+ errorMessage="Value must be a positive number."
271
+ />
272
+ ),
273
+ };
274
+
275
+ export const ErrorSelect: Story = {
276
+ name: 'Error – Select Field',
277
+ render: () => (
278
+ <FieldWrapper
279
+ Component={SelectField}
280
+ field={{
281
+ label: 'Priority',
282
+ type: 'select',
283
+ options: [
284
+ { label: 'Low', value: 'low' },
285
+ { label: 'Medium', value: 'medium' },
286
+ { label: 'High', value: 'high' },
287
+ ],
288
+ }}
289
+ initialValue=""
290
+ errorMessage="Please select a priority."
291
+ />
292
+ ),
293
+ };
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // 6. Disabled State
297
+ // ---------------------------------------------------------------------------
298
+
299
+ export const DisabledText: Story = {
300
+ name: 'Disabled – Text Field',
301
+ render: () => (
302
+ <FieldWrapper
303
+ Component={TextField}
304
+ field={{ label: 'Locked Name', type: 'text' }}
305
+ initialValue="Cannot change this"
306
+ disabled={true}
307
+ />
308
+ ),
309
+ };
310
+
311
+ export const DisabledNumber: Story = {
312
+ name: 'Disabled – Number Field',
313
+ render: () => (
314
+ <FieldWrapper
315
+ Component={NumberField}
316
+ field={{ label: 'Locked Qty', type: 'number', precision: 0 }}
317
+ initialValue={100}
318
+ disabled={true}
319
+ />
320
+ ),
321
+ };
322
+
323
+ export const DisabledSelect: Story = {
324
+ name: 'Disabled – Select Field',
325
+ render: () => (
326
+ <FieldWrapper
327
+ Component={SelectField}
328
+ field={{
329
+ label: 'Locked Status',
330
+ type: 'select',
331
+ options: [
332
+ { label: 'Active', value: 'active' },
333
+ { label: 'Inactive', value: 'inactive' },
334
+ ],
335
+ }}
336
+ initialValue="active"
337
+ disabled={true}
338
+ />
339
+ ),
340
+ };
341
+
342
+ export const DisabledBoolean: Story = {
343
+ name: 'Disabled – Boolean Field',
344
+ render: () => (
345
+ <FieldWrapper
346
+ Component={BooleanField}
347
+ field={{ label: 'Locked Toggle', type: 'boolean' }}
348
+ initialValue={true}
349
+ disabled={true}
350
+ />
351
+ ),
352
+ };
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // 7. Readonly State with Various Field Types
356
+ // ---------------------------------------------------------------------------
357
+
358
+ export const ReadonlyGallery: Story = {
359
+ name: 'Readonly – Multiple Fields',
360
+ render: () => (
361
+ <div className="flex flex-col gap-4">
362
+ <FieldWrapper
363
+ Component={TextField}
364
+ field={{ label: 'Read Only Text', type: 'text' }}
365
+ initialValue="Immutable value"
366
+ readonly={true}
367
+ />
368
+ <FieldWrapper
369
+ Component={NumberField}
370
+ field={{ label: 'Read Only Number', type: 'number', precision: 2 }}
371
+ initialValue={3.14}
372
+ readonly={true}
373
+ />
374
+ <FieldWrapper
375
+ Component={CurrencyField}
376
+ field={{ label: 'Read Only Currency', type: 'currency', currency: 'USD', precision: 2 }}
377
+ initialValue={9999.99}
378
+ readonly={true}
379
+ />
380
+ <FieldWrapper
381
+ Component={SelectField}
382
+ field={{
383
+ label: 'Read Only Select',
384
+ type: 'select',
385
+ options: [
386
+ { label: 'Active', value: 'active' },
387
+ { label: 'Archived', value: 'archived' },
388
+ ],
389
+ }}
390
+ initialValue="active"
391
+ readonly={true}
392
+ />
393
+ <FieldWrapper
394
+ Component={BooleanField}
395
+ field={{ label: 'Read Only Boolean', type: 'boolean' }}
396
+ initialValue={false}
397
+ readonly={true}
398
+ />
399
+ <FieldWrapper
400
+ Component={EmailField}
401
+ field={{ label: 'Read Only Email', type: 'email' }}
402
+ initialValue="admin@example.com"
403
+ readonly={true}
404
+ />
405
+ </div>
406
+ ),
407
+ };
package/src/index.tsx CHANGED
@@ -76,9 +76,12 @@ export function formatCurrency(value: number, currency: string = 'USD'): string
76
76
 
77
77
  /**
78
78
  * Format percent value
79
+ * Handles both decimal (0.8 = 80%) and whole number (80 = 80%) inputs.
79
80
  */
80
- export function formatPercent(value: number, precision: number = 2): string {
81
- return `${(value * 100).toFixed(precision)}%`;
81
+ export function formatPercent(value: number, precision: number = 0): string {
82
+ // If value is between -1 and 1 (exclusive), treat as decimal (e.g. 0.8 → 80%)
83
+ const displayValue = (value > -1 && value < 1) ? value * 100 : value;
84
+ return `${displayValue.toFixed(precision)}%`;
82
85
  }
83
86
 
84
87
  /**
@@ -125,11 +128,13 @@ export function TextCellRenderer({ value }: CellRendererProps): React.ReactEleme
125
128
  * Number field cell renderer
126
129
  */
127
130
  export function NumberCellRenderer({ value, field }: CellRendererProps): React.ReactElement {
128
- if (value == null) return <span>-</span>;
131
+ if (value == null) return <span className="text-muted-foreground">-</span>;
129
132
 
130
133
  const numField = field as any;
131
134
  const precision = numField.precision ?? 0;
132
- const formatted = typeof value === 'number' ? value.toFixed(precision) : value;
135
+ const formatted = typeof value === 'number'
136
+ ? new Intl.NumberFormat('en-US', { minimumFractionDigits: precision, maximumFractionDigits: precision }).format(value)
137
+ : value;
133
138
 
134
139
  return <span className="tabular-nums">{formatted}</span>;
135
140
  }
@@ -138,26 +143,46 @@ export function NumberCellRenderer({ value, field }: CellRendererProps): React.R
138
143
  * Currency field cell renderer
139
144
  */
140
145
  export function CurrencyCellRenderer({ value, field }: CellRendererProps): React.ReactElement {
141
- if (value == null) return <span>-</span>;
146
+ if (value == null) return <span className="text-muted-foreground">-</span>;
142
147
 
143
148
  const currencyField = field as any;
144
149
  const currency = currencyField.currency || 'USD';
145
150
  const formatted = formatCurrency(Number(value), currency);
146
151
 
147
- return <span className="tabular-nums font-medium">{formatted}</span>;
152
+ return <span className="tabular-nums font-medium whitespace-nowrap">{formatted}</span>;
148
153
  }
149
154
 
150
155
  /**
151
- * Percent field cell renderer
156
+ * Percent field cell renderer with mini progress bar
152
157
  */
153
158
  export function PercentCellRenderer({ value, field }: CellRendererProps): React.ReactElement {
154
- if (value == null) return <span>-</span>;
159
+ if (value == null) return <span className="text-muted-foreground">-</span>;
155
160
 
156
161
  const percentField = field as any;
157
- const precision = percentField.precision ?? 2;
158
- const formatted = formatPercent(Number(value), precision);
162
+ const precision = percentField.precision ?? 0;
163
+ const numValue = Number(value);
164
+ const formatted = formatPercent(numValue, precision);
165
+ // Normalize to 0-100 range for progress bar
166
+ const barValue = (numValue > -1 && numValue < 1) ? numValue * 100 : numValue;
167
+ const clampedBar = Math.max(0, Math.min(100, barValue));
159
168
 
160
- return <span className="tabular-nums">{formatted}</span>;
169
+ return (
170
+ <div className="flex items-center gap-2">
171
+ <div
172
+ className="h-1.5 w-16 rounded-full bg-muted overflow-hidden flex-shrink-0"
173
+ role="progressbar"
174
+ aria-valuenow={clampedBar}
175
+ aria-valuemin={0}
176
+ aria-valuemax={100}
177
+ >
178
+ <div
179
+ className="h-full rounded-full bg-primary transition-all"
180
+ style={{ width: `${clampedBar}%` }}
181
+ />
182
+ </div>
183
+ <span className="tabular-nums whitespace-nowrap">{formatted}</span>
184
+ </div>
185
+ );
161
186
  }
162
187
 
163
188
  /**
@@ -2,6 +2,10 @@ import React, { useId } from 'react';
2
2
  import { Switch, Checkbox, Label } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * BooleanField - Toggle input supporting switch and checkbox variants
7
+ * Renders as Switch or Checkbox based on field widget configuration
8
+ */
5
9
  export function BooleanField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<boolean>) {
6
10
  const config = (field || (props as any).schema) as any;
7
11
  // Use simple type assertion for arbitrary custom properties not in BaseFieldMetadata
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * DateField - Date picker input widget
7
+ * Uses native date input and displays locale-formatted date in readonly mode
8
+ */
5
9
  export function DateField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
6
10
  if (readonly) {
7
11
  return <span className="text-sm">{value ? new Date(value).toLocaleDateString() : '-'}</span>;
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * DateTimeField - Combined date and time picker widget
7
+ * Displays both date and time in locale format when readonly
8
+ */
5
9
  export function DateTimeField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
6
10
  if (readonly) {
7
11
  if (!value) return <span className="text-sm">-</span>;
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * EmailField - Email address input with mailto link in readonly mode
7
+ * Renders as a clickable mailto link when readonly, standard email input otherwise
8
+ */
5
9
  export function EmailField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
6
10
  const config = field || (props as any).schema;
7
11
  if (readonly) {
@@ -3,6 +3,10 @@ import { Button } from '@object-ui/components';
3
3
  import { Upload, X, File as FileIcon } from 'lucide-react';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
6
+ /**
7
+ * FileField - File upload widget with drag-and-drop support
8
+ * Supports single and multiple file uploads with configurable accepted file types
9
+ */
6
10
  export function FileField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<any>) {
7
11
  const inputRef = useRef<HTMLInputElement>(null);
8
12
  const fileField = (field || (props as any).schema) as any;
@@ -3,6 +3,10 @@ import { Button } from '@object-ui/components';
3
3
  import { Upload, X, Image as ImageIcon } from 'lucide-react';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
6
+ /**
7
+ * ImageField - Image upload widget with preview thumbnails
8
+ * Supports single and multiple image uploads with drag-and-drop and preview display
9
+ */
6
10
  export function ImageField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<any>) {
7
11
  const inputRef = useRef<HTMLInputElement>(null);
8
12
  const imageField = (field || (props as any).schema) as any;
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * LocationField - Geographic coordinate input for latitude and longitude
7
+ * Stores location as { latitude, longitude } object and displays as comma-separated pair
8
+ */
5
9
  export function LocationField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<any>) {
6
10
  const config = field || (props as any).schema;
7
11
  // Location is stored as { latitude, longitude } object
@@ -3,6 +3,10 @@ import { Input } from '@object-ui/components';
3
3
  import { NumberFieldMetadata } from '@object-ui/types';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
6
+ /**
7
+ * NumberField - Numeric input with optional decimal precision
8
+ * Supports min/max/step constraints and configurable decimal precision
9
+ */
6
10
  export function NumberField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<number>) {
7
11
  if (readonly) {
8
12
  return <span className="text-sm">{value ?? '-'}</span>;
@@ -4,6 +4,10 @@ import { Button } from '@object-ui/components';
4
4
  import { Eye, EyeOff } from 'lucide-react';
5
5
  import { FieldWidgetProps } from './types';
6
6
 
7
+ /**
8
+ * PasswordField - Secure password input with visibility toggle
9
+ * Includes show/hide toggle button and masks value in readonly mode
10
+ */
7
11
  export function PasswordField({ value, onChange, field, readonly, className, ...props }: FieldWidgetProps<string>) {
8
12
  const [showPassword, setShowPassword] = useState(false);
9
13
  const config = field || (props as any).schema;
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * PercentField - Percentage input with configurable decimal precision
7
+ * Stores values as decimals (0-1) and displays as percentages (0-100%)
8
+ */
5
9
  export function PercentField({ value, onChange, field, readonly, errorMessage, className, ...props }: FieldWidgetProps<number>) {
6
10
  const percentField = (field || (props as any).schema) as any;
7
11
  const precision = percentField?.precision ?? 2;
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * PhoneField - Telephone number input with tel link in readonly mode
7
+ * Renders as a clickable tel link when readonly, standard phone input otherwise
8
+ */
5
9
  export function PhoneField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
6
10
  const config = field || (props as any).schema;
7
11
  if (readonly) {
@@ -9,6 +9,10 @@ import {
9
9
  import { SelectFieldMetadata } from '@object-ui/types';
10
10
  import { FieldWidgetProps } from './types';
11
11
 
12
+ /**
13
+ * SelectField - Dropdown selection widget with configurable options
14
+ * Renders options from field metadata with support for placeholder and readonly display
15
+ */
12
16
  export function SelectField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
13
17
  const config = (field || (props as any).schema) as SelectFieldMetadata;
14
18
  const options = config?.options || [];
@@ -94,7 +94,7 @@ export function SignatureField({ value, onChange, readonly }: FieldWidgetProps<s
94
94
  if (readonly && value) {
95
95
  return (
96
96
  <div className="border rounded p-2 bg-white">
97
- <img src={value} alt="Signature" className="max-w-full h-auto" />
97
+ <img src={value} alt="Signature" loading="lazy" className="max-w-full h-auto" />
98
98
  </div>
99
99
  );
100
100
  }
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Textarea } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * TextAreaField - Multi-line text input widget
7
+ * Supports configurable row count and preserves whitespace in readonly mode
8
+ */
5
9
  export function TextAreaField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
6
10
  if (readonly) {
7
11
  return (
@@ -3,6 +3,10 @@ import { Input, Textarea } from '@object-ui/components';
3
3
  import { TextareaFieldMetadata } from '@object-ui/types';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
6
+ /**
7
+ * TextField - Standard single-line or multi-line text input
8
+ * Automatically renders as a textarea when rows are configured in field metadata
9
+ */
6
10
  export function TextField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
7
11
  const fieldData = field || (props as any).schema;
8
12
 
@@ -2,6 +2,10 @@ import React from 'react';
2
2
  import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
+ /**
6
+ * TimeField - Time picker input widget
7
+ * Uses native time input for hour and minute selection
8
+ */
5
9
  export function TimeField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
6
10
  if (readonly) {
7
11
  return <span className="text-sm">{value || '-'}</span>;