@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,96 @@
1
+ import React from 'react';
2
+ import { Input, Button } from '@object-ui/components';
3
+ import { QrCode, Copy } from 'lucide-react';
4
+ import { FieldWidgetProps } from './types';
5
+
6
+ /**
7
+ * QR Code field widget - generates QR codes from text
8
+ * Uses a simple SVG-based QR code generator
9
+ */
10
+ export function QRCodeField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
11
+ const [showQR, setShowQR] = React.useState(false);
12
+ const config = field || (props as any).schema;
13
+
14
+ // Simple QR code generation using an external library would be ideal
15
+ // For now, we'll use a service API approach or placeholder
16
+ const getQRCodeUrl = (text: string): string => {
17
+ // Using a simple QR code API service (you might want to replace this with a library)
18
+ return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(text)}`;
19
+ };
20
+
21
+ const copyToClipboard = () => {
22
+ if (value) {
23
+ navigator.clipboard.writeText(value);
24
+ }
25
+ };
26
+
27
+ if (readonly) {
28
+ return (
29
+ <div className="flex items-center gap-3">
30
+ <span className="text-sm flex-1 truncate">{value || '-'}</span>
31
+ {value && (
32
+ <Button
33
+ type="button"
34
+ variant="outline"
35
+ size="sm"
36
+ onClick={() => setShowQR(!showQR)}
37
+ >
38
+ <QrCode className="w-4 h-4 mr-2" />
39
+ {showQR ? 'Hide' : 'Show'} QR
40
+ </Button>
41
+ )}
42
+ </div>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <div className="space-y-3">
48
+ <div className="flex items-center gap-2">
49
+ <Input
50
+ type="text"
51
+ value={value || ''}
52
+ onChange={(e) => onChange(e.target.value)}
53
+ placeholder={config?.placeholder || 'Enter text for QR code'}
54
+ disabled={readonly || props.disabled}
55
+ className={props.className}
56
+ />
57
+ {value && (
58
+ <>
59
+ <Button
60
+ type="button"
61
+ variant="outline"
62
+ size="sm"
63
+ onClick={copyToClipboard}
64
+ >
65
+ <Copy className="w-4 h-4" />
66
+ </Button>
67
+ <Button
68
+ type="button"
69
+ variant="outline"
70
+ size="sm"
71
+ onClick={() => setShowQR(!showQR)}
72
+ >
73
+ <QrCode className="w-4 h-4" />
74
+ </Button>
75
+ </>
76
+ )}
77
+ </div>
78
+
79
+ {showQR && value && (
80
+ <div className="border rounded p-4 bg-white flex justify-center">
81
+ <img
82
+ src={getQRCodeUrl(value)}
83
+ alt="QR Code"
84
+ className="w-48 h-48"
85
+ />
86
+ </div>
87
+ )}
88
+
89
+ {value && (
90
+ <p className="text-xs text-muted-foreground">
91
+ QR code contains: {value.length} characters
92
+ </p>
93
+ )}
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Star } from 'lucide-react';
3
+ import { cn } from '@object-ui/components';
4
+ import { FieldWidgetProps } from './types';
5
+
6
+ /**
7
+ * Rating field widget - provides a star rating input
8
+ * Supports numeric values from 0 to max (default 5)
9
+ */
10
+ export function RatingField({ value, onChange, field, readonly, className, ...props }: FieldWidgetProps<number>) {
11
+ // Get rating-specific configuration from field metadata
12
+ const ratingField = (field || (props as any).schema) as any;
13
+ const max = ratingField?.max ?? 5;
14
+ const currentValue = value ?? 0;
15
+
16
+ const [hoverValue, setHoverValue] = React.useState<number | null>(null);
17
+
18
+ const displayValue = hoverValue !== null ? hoverValue : currentValue;
19
+
20
+ if (readonly) {
21
+ return (
22
+ <div className={cn("flex items-center gap-1", className)}>
23
+ {Array.from({ length: max }, (_, i) => (
24
+ <Star
25
+ key={i}
26
+ className={`w-5 h-5 ${
27
+ i < currentValue
28
+ ? 'fill-yellow-400 text-yellow-400'
29
+ : 'text-muted-foreground'
30
+ }`}
31
+ />
32
+ ))}
33
+ <span className="ml-2 text-sm text-muted-foreground">
34
+ {currentValue} / {max}
35
+ </span>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <div className={cn("flex items-center gap-1", className)}>
42
+ {Array.from({ length: max }, (_, i) => (
43
+ <button
44
+ key={i}
45
+ type="button"
46
+ onClick={() => onChange(i + 1)}
47
+ onMouseEnter={() => setHoverValue(i + 1)}
48
+ onMouseLeave={() => setHoverValue(null)}
49
+ className="focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded"
50
+ disabled={readonly || props.disabled}
51
+ >
52
+ <Star
53
+ className={`w-5 h-5 transition-colors ${
54
+ i < displayValue
55
+ ? 'fill-yellow-400 text-yellow-400 hover:fill-yellow-500 hover:text-yellow-500'
56
+ : 'text-muted-foreground hover:text-yellow-400'
57
+ }`}
58
+ />
59
+ </button>
60
+ ))}
61
+ <span className="ml-2 text-sm text-muted-foreground">
62
+ {currentValue} / {max}
63
+ </span>
64
+ </div>
65
+ );
66
+ }
@@ -18,9 +18,9 @@ export function RichTextField({ value, onChange, field, readonly, errorMessage,
18
18
  );
19
19
  }
20
20
 
21
- const richField = field as any;
22
- const rows = richField.rows || 8;
23
- const format = richField.format || 'markdown'; // 'markdown' or 'html'
21
+ const richField = (field || (props as any).schema) as any;
22
+ const rows = richField?.rows || 8;
23
+ const format = richField?.format || 'markdown'; // 'markdown' or 'html'
24
24
 
25
25
  return (
26
26
  <div className="space-y-2">
@@ -31,8 +31,8 @@ export function RichTextField({ value, onChange, field, readonly, errorMessage,
31
31
  <Textarea
32
32
  value={value || ''}
33
33
  onChange={(e) => onChange(e.target.value)}
34
- placeholder={field.placeholder || 'Enter text...'}
35
- disabled={readonly}
34
+ placeholder={richField?.placeholder || 'Enter text...'}
35
+ disabled={readonly || props.disabled}
36
36
  rows={rows}
37
37
  className={`font-mono text-sm ${props.className || ''}`}
38
38
  aria-invalid={!!errorMessage}
@@ -10,7 +10,8 @@ import { SelectFieldMetadata } from '@object-ui/types';
10
10
  import { FieldWidgetProps } from './types';
11
11
 
12
12
  export function SelectField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
13
- const options = (field as unknown as SelectFieldMetadata).options || [];
13
+ const config = (field || (props as any).schema) as SelectFieldMetadata;
14
+ const options = config?.options || [];
14
15
 
15
16
  if (readonly) {
16
17
  const option = options.find((o) => o.value === value);
@@ -19,14 +20,15 @@ export function SelectField({ value, onChange, field, readonly, ...props }: Fiel
19
20
 
20
21
  return (
21
22
  <Select
23
+ {...props}
22
24
  value={value}
23
25
  onValueChange={onChange}
24
- disabled={readonly}
26
+ disabled={readonly || props.disabled}
25
27
  >
26
- <SelectTrigger className={props.className}>
27
- <SelectValue placeholder={field.placeholder || "Select an option"} />
28
+ <SelectTrigger className={props.className} id={props.id}>
29
+ <SelectValue placeholder={config?.placeholder || "Select an option"} />
28
30
  </SelectTrigger>
29
- <SelectContent>
31
+ <SelectContent position="popper">
30
32
  {options.map((option) => (
31
33
  <SelectItem key={option.value} value={option.value}>
32
34
  {option.label}
@@ -0,0 +1,140 @@
1
+ import React, { useRef, useEffect } from 'react';
2
+ import { Button } from '@object-ui/components';
3
+ import { Eraser } from 'lucide-react';
4
+ import { FieldWidgetProps } from './types';
5
+
6
+ /**
7
+ * Signature field widget - provides a signature pad for capturing signatures
8
+ * Outputs signature as a base64 PNG image
9
+ */
10
+ export function SignatureField({ value, onChange, readonly }: FieldWidgetProps<string>) {
11
+ const canvasRef = useRef<HTMLCanvasElement>(null);
12
+ const [isDrawing, setIsDrawing] = React.useState(false);
13
+ const [isEmpty, setIsEmpty] = React.useState(!value);
14
+
15
+ useEffect(() => {
16
+ if (value && canvasRef.current) {
17
+ const canvas = canvasRef.current;
18
+ const ctx = canvas.getContext('2d');
19
+ if (ctx) {
20
+ const img = new Image();
21
+ img.onload = () => {
22
+ ctx.drawImage(img, 0, 0);
23
+ setIsEmpty(false);
24
+ };
25
+ img.src = value;
26
+ }
27
+ }
28
+ }, [value]);
29
+
30
+ const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
31
+ if (readonly) return;
32
+ setIsDrawing(true);
33
+ setIsEmpty(false);
34
+
35
+ const canvas = canvasRef.current;
36
+ if (!canvas) return;
37
+
38
+ const ctx = canvas.getContext('2d');
39
+ if (!ctx) return;
40
+
41
+ const rect = canvas.getBoundingClientRect();
42
+ const x = 'touches' in e ? e.touches[0].clientX - rect.left : e.clientX - rect.left;
43
+ const y = 'touches' in e ? e.touches[0].clientY - rect.top : e.clientY - rect.top;
44
+
45
+ ctx.beginPath();
46
+ ctx.moveTo(x, y);
47
+ };
48
+
49
+ const draw = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
50
+ if (!isDrawing || readonly) return;
51
+
52
+ const canvas = canvasRef.current;
53
+ if (!canvas) return;
54
+
55
+ const ctx = canvas.getContext('2d');
56
+ if (!ctx) return;
57
+
58
+ const rect = canvas.getBoundingClientRect();
59
+ const x = 'touches' in e ? e.touches[0].clientX - rect.left : e.clientX - rect.left;
60
+ const y = 'touches' in e ? e.touches[0].clientY - rect.top : e.clientY - rect.top;
61
+
62
+ ctx.lineTo(x, y);
63
+ ctx.strokeStyle = '#000';
64
+ ctx.lineWidth = 2;
65
+ ctx.lineCap = 'round';
66
+ ctx.lineJoin = 'round';
67
+ ctx.stroke();
68
+ };
69
+
70
+ const stopDrawing = () => {
71
+ if (!isDrawing) return;
72
+ setIsDrawing(false);
73
+
74
+ const canvas = canvasRef.current;
75
+ if (!canvas) return;
76
+
77
+ // Save signature as base64 PNG
78
+ const dataUrl = canvas.toDataURL('image/png');
79
+ onChange(dataUrl);
80
+ };
81
+
82
+ const clearSignature = () => {
83
+ const canvas = canvasRef.current;
84
+ if (!canvas) return;
85
+
86
+ const ctx = canvas.getContext('2d');
87
+ if (!ctx) return;
88
+
89
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
90
+ setIsEmpty(true);
91
+ onChange('');
92
+ };
93
+
94
+ if (readonly && value) {
95
+ return (
96
+ <div className="border rounded p-2 bg-white">
97
+ <img src={value} alt="Signature" className="max-w-full h-auto" />
98
+ </div>
99
+ );
100
+ }
101
+
102
+ if (readonly && !value) {
103
+ return <span className="text-sm text-muted-foreground">No signature</span>;
104
+ }
105
+
106
+ return (
107
+ <div className="space-y-2">
108
+ <div className="border rounded bg-white">
109
+ <canvas
110
+ ref={canvasRef}
111
+ width={400}
112
+ height={200}
113
+ className="w-full touch-none cursor-crosshair"
114
+ onMouseDown={startDrawing}
115
+ onMouseMove={draw}
116
+ onMouseUp={stopDrawing}
117
+ onMouseLeave={stopDrawing}
118
+ onTouchStart={startDrawing}
119
+ onTouchMove={draw}
120
+ onTouchEnd={stopDrawing}
121
+ />
122
+ </div>
123
+ <div className="flex justify-between items-center">
124
+ <p className="text-xs text-muted-foreground">
125
+ {isEmpty ? 'Sign above' : 'Signature captured'}
126
+ </p>
127
+ <Button
128
+ type="button"
129
+ variant="outline"
130
+ size="sm"
131
+ onClick={clearSignature}
132
+ disabled={readonly || isEmpty}
133
+ >
134
+ <Eraser className="w-4 h-4 mr-2" />
135
+ Clear
136
+ </Button>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Slider } from '@object-ui/components';
3
+ import { FieldWidgetProps } from './types';
4
+
5
+ /**
6
+ * Slider field widget - provides a range slider input
7
+ * Supports numeric values with configurable min, max, and step
8
+ */
9
+ export function SliderField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<number>) {
10
+ // Get slider-specific configuration from field metadata
11
+ const sliderField = (field || (props as any).schema) as any;
12
+ const min = sliderField?.min ?? 0;
13
+ const max = sliderField?.max ?? 100;
14
+ const step = sliderField?.step ?? 1;
15
+
16
+ if (readonly) {
17
+ return (
18
+ <div className="flex items-center gap-2">
19
+ <span className="text-sm font-medium">{value ?? min}</span>
20
+ <span className="text-xs text-muted-foreground">/ {max}</span>
21
+ </div>
22
+ );
23
+ }
24
+
25
+ return (
26
+ <div className="flex items-center gap-4">
27
+ <Slider
28
+ value={[value ?? min]}
29
+ onValueChange={(values) => onChange(values[0])}
30
+ min={min}
31
+ max={max}
32
+ step={step}
33
+ disabled={readonly || props.disabled}
34
+ className={props.className}
35
+ />
36
+ <span className="text-sm font-medium w-12 text-right">{value ?? min}</span>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { FieldWidgetProps } from './types';
3
+
4
+ /**
5
+ * SummaryField - Read-only aggregation field
6
+ * Values are aggregated from related records and cannot be edited
7
+ */
8
+ export function SummaryField({ value, field, ...props }: FieldWidgetProps<any>) {
9
+ const summaryField = (field || (props as any).schema) as any;
10
+ const summaryType = summaryField?.summary_type || 'count';
11
+
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
+ }
22
+ }
23
+
24
+ return (
25
+ <span className={`text-sm font-medium tabular-nums text-gray-700 ${props.className || ''}`}>
26
+ {displayValue}
27
+ </span>
28
+ );
29
+ }
@@ -11,20 +11,23 @@ export function TextAreaField({ value, onChange, field, readonly, errorMessage,
11
11
  );
12
12
  }
13
13
 
14
- const textareaField = field as any;
15
- const rows = textareaField.rows || 4;
16
- const maxLength = textareaField.max_length;
14
+ const textareaField = (field || (props as any).schema) as any;
15
+ const rows = textareaField?.rows || 4;
16
+ const maxLength = textareaField?.max_length;
17
+
18
+ // Filter out non-DOM props
19
+ const { inputType, ...domProps } = props as any;
17
20
 
18
21
  return (
19
22
  <div className="relative">
20
23
  <Textarea
24
+ {...domProps}
21
25
  value={value || ''}
22
26
  onChange={(e) => onChange(e.target.value)}
23
- placeholder={field.placeholder}
24
- disabled={readonly}
27
+ placeholder={textareaField?.placeholder}
28
+ disabled={readonly || domProps.disabled}
25
29
  rows={rows}
26
30
  maxLength={maxLength}
27
- className={props.className}
28
31
  aria-invalid={!!errorMessage}
29
32
  />
30
33
  {maxLength && (
@@ -4,33 +4,38 @@ import { TextareaFieldMetadata } from '@object-ui/types';
4
4
  import { FieldWidgetProps } from './types';
5
5
 
6
6
  export function TextField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
7
+ const fieldData = field || (props as any).schema;
8
+
7
9
  if (readonly) {
8
10
  return <span className="text-sm">{value || '-'}</span>;
9
11
  }
10
12
 
11
13
  // Cast for rows property
12
- const rows = (field as unknown as TextareaFieldMetadata).rows;
14
+ const rows = (fieldData as unknown as TextareaFieldMetadata)?.rows;
15
+
16
+ // Filter out non-DOM props
17
+ const { inputType, ...domProps } = props as any;
13
18
 
14
19
  if (rows && rows > 1) {
15
20
  return (
16
21
  <Textarea
22
+ {...domProps}
17
23
  value={value || ''}
18
24
  onChange={(e) => onChange(e.target.value)}
19
- placeholder={field.placeholder}
20
- disabled={readonly}
21
- className={props.className}
25
+ placeholder={fieldData?.placeholder}
26
+ disabled={readonly || domProps.disabled}
22
27
  />
23
28
  );
24
29
  }
25
30
 
26
31
  return (
27
32
  <Input
28
- type={field.type === 'password' ? 'password' : 'text'}
33
+ {...domProps}
34
+ type={fieldData?.type === 'password' ? 'password' : 'text'}
29
35
  value={value || ''}
30
36
  onChange={(e) => onChange(e.target.value)}
31
- placeholder={field.placeholder}
32
- disabled={readonly}
33
- className={props.className}
37
+ placeholder={fieldData?.placeholder}
38
+ disabled={readonly || domProps.disabled}
34
39
  />
35
40
  );
36
41
  }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { Input } from '@object-ui/components';
3
+ import { FieldWidgetProps } from './types';
4
+
5
+ export function TimeField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
6
+ if (readonly) {
7
+ return <span className="text-sm">{value || '-'}</span>;
8
+ }
9
+
10
+ // Filter out non-DOM props
11
+ const { inputType, ...domProps } = props as any;
12
+
13
+ return (
14
+ <Input
15
+ {...domProps}
16
+ type="time"
17
+ value={value || ''}
18
+ onChange={(e) => onChange(e.target.value)}
19
+ disabled={readonly || domProps.disabled}
20
+ />
21
+ );
22
+ }
@@ -3,6 +3,7 @@ import { Input } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  export function UrlField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
6
+ const config = field || (props as any).schema;
6
7
  if (readonly) {
7
8
  if (!value) return <span className="text-sm">-</span>;
8
9
 
@@ -24,14 +25,17 @@ export function UrlField({ value, onChange, field, readonly, errorMessage, ...pr
24
25
  );
25
26
  }
26
27
 
28
+ // Filter out non-DOM props
29
+ const { inputType, ...domProps } = props as any;
30
+
27
31
  return (
28
32
  <Input
33
+ {...domProps}
29
34
  type="url"
30
35
  value={value || ''}
31
36
  onChange={(e) => onChange(e.target.value)}
32
- placeholder={field.placeholder || 'https://example.com'}
33
- disabled={readonly}
34
- className={props.className}
37
+ placeholder={config?.placeholder || 'https://example.com'}
38
+ disabled={readonly || domProps.disabled}
35
39
  aria-invalid={!!errorMessage}
36
40
  />
37
41
  );
@@ -0,0 +1,88 @@
1
+ import React from 'react';
2
+ import { Avatar, AvatarFallback, Badge } from '@object-ui/components';
3
+ import { X } from 'lucide-react';
4
+ import { FieldWidgetProps } from './types';
5
+
6
+ /**
7
+ * UserField - User/Owner selector field
8
+ * Allows selecting one or multiple users
9
+ */
10
+ export function UserField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<any>) {
11
+ const userField = (field || (props as any).schema) as any;
12
+ const multiple = userField?.multiple || false;
13
+
14
+ if (readonly) {
15
+ if (!value) return <span className="text-sm">-</span>;
16
+
17
+ const users = Array.isArray(value) ? value : [value];
18
+ return (
19
+ <div className="flex -space-x-2">
20
+ {users.slice(0, 3).map((user: any, idx: number) => {
21
+ const name = user.name || user.username || 'User';
22
+ const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
23
+
24
+ return (
25
+ <Avatar key={idx} className="size-8 border-2 border-white" title={name}>
26
+ <AvatarFallback className="bg-blue-500 text-white text-xs">
27
+ {initials}
28
+ </AvatarFallback>
29
+ </Avatar>
30
+ );
31
+ })}
32
+ {users.length > 3 && (
33
+ <Avatar className="size-8 border-2 border-white">
34
+ <AvatarFallback className="bg-gray-200 text-gray-600 text-xs">
35
+ +{users.length - 3}
36
+ </AvatarFallback>
37
+ </Avatar>
38
+ )}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ const users = value ? (Array.isArray(value) ? value : [value]) : [];
44
+
45
+ const handleRemove = (index: number) => {
46
+ if (multiple) {
47
+ const newUsers = users.filter((_: any, i: number) => i !== index);
48
+ onChange(newUsers.length > 0 ? newUsers : null);
49
+ } else {
50
+ onChange(null);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <div className={props.className}>
56
+ {users.length > 0 && (
57
+ <div className="flex flex-wrap gap-2 mb-2">
58
+ {users.map((user: any, idx: number) => {
59
+ const name = user.name || user.username || 'User';
60
+ const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
61
+
62
+ return (
63
+ <Badge key={idx} variant="outline" className="gap-2 pr-1">
64
+ <Avatar className="size-5">
65
+ <AvatarFallback className="bg-blue-500 text-white text-xs">
66
+ {initials}
67
+ </AvatarFallback>
68
+ </Avatar>
69
+ <span className="text-sm">{name}</span>
70
+ <button
71
+ type="button"
72
+ onClick={() => handleRemove(idx)}
73
+ className="ml-1 rounded-full hover:bg-gray-200 p-0.5"
74
+ >
75
+ <X className="size-3" />
76
+ </button>
77
+ </Badge>
78
+ );
79
+ })}
80
+ </div>
81
+ )}
82
+
83
+ <div className="text-sm text-gray-500 italic">
84
+ User selection component requires integration with user management system
85
+ </div>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { FieldWidgetProps } from './types';
3
+
4
+ /**
5
+ * VectorField - Vector/embedding data display
6
+ * Shows vector embeddings in a read-only format
7
+ */
8
+ export function VectorField({ value, field, ...props }: FieldWidgetProps<number[]>) {
9
+ const vectorField = (field || (props as any).schema) as any;
10
+ const dimensions = vectorField?.dimensions || (Array.isArray(value) ? value.length : 0);
11
+
12
+ if (!value || !Array.isArray(value)) {
13
+ return <span className="text-sm text-gray-500">-</span>;
14
+ }
15
+
16
+ // Show first few values and total dimensions
17
+ const preview = value.slice(0, 3).map(v => {
18
+ const num = Number(v);
19
+ return isNaN(num) ? '0' : num.toFixed(4);
20
+ }).join(', ');
21
+
22
+ return (
23
+ <div className={`text-sm ${props.className || ''}`}>
24
+ <span className="font-mono text-gray-700">[{preview}...]</span>
25
+ <span className="text-gray-500 ml-2">({dimensions}D)</span>
26
+ </div>
27
+ );
28
+ }
@@ -6,6 +6,8 @@ export type FieldWidgetProps<T = any> = {
6
6
  // Use a looser type for field to avoid complex circular dependencies for now
7
7
  field: FieldMetadata;
8
8
  readonly?: boolean;
9
+ disabled?: boolean;
9
10
  className?: string;
10
11
  errorMessage?: string;
12
+ [key: string]: any;
11
13
  }