@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.
- package/dist/fields/src/complex-widgets.test.d.ts +1 -0
- package/dist/fields/src/datetime-widgets.test.d.ts +1 -0
- package/dist/fields/src/fields.stories.d.ts +56 -0
- package/dist/fields/src/index.d.ts +48 -0
- package/dist/fields/src/new-widgets.test.d.ts +8 -0
- package/dist/fields/src/remaining-widgets.test.d.ts +1 -0
- package/dist/fields/src/standard-widgets.test.d.ts +0 -0
- package/dist/fields/src/text-rich-widgets.test.d.ts +1 -0
- package/dist/fields/src/visual-widgets.test.d.ts +1 -0
- package/dist/fields/src/widgets/AddressField.d.ts +16 -0
- package/dist/fields/src/widgets/AutoNumberField.d.ts +6 -0
- package/dist/fields/src/widgets/AvatarField.d.ts +6 -0
- package/dist/fields/src/widgets/CodeField.d.ts +7 -0
- package/dist/fields/src/widgets/ColorField.d.ts +6 -0
- package/dist/fields/src/widgets/CurrencyField.d.ts +1 -1
- package/dist/fields/src/widgets/DateTimeField.d.ts +2 -0
- package/dist/fields/src/widgets/FileField.d.ts +2 -0
- package/dist/fields/src/widgets/FormulaField.d.ts +6 -0
- package/dist/fields/src/widgets/GeolocationField.d.ts +14 -0
- package/dist/fields/src/widgets/GridField.d.ts +7 -0
- package/dist/fields/src/widgets/ImageField.d.ts +2 -0
- package/dist/fields/src/widgets/LocationField.d.ts +2 -0
- package/dist/fields/src/widgets/LookupField.d.ts +1 -1
- package/dist/fields/src/widgets/MasterDetailField.d.ts +14 -0
- package/dist/fields/src/widgets/ObjectField.d.ts +6 -0
- package/dist/fields/src/widgets/PasswordField.d.ts +2 -0
- package/dist/fields/src/widgets/PercentField.d.ts +2 -0
- package/dist/fields/src/widgets/QRCodeField.d.ts +6 -0
- package/dist/fields/src/widgets/RatingField.d.ts +6 -0
- package/dist/fields/src/widgets/SignatureField.d.ts +6 -0
- package/dist/fields/src/widgets/SliderField.d.ts +6 -0
- package/dist/fields/src/widgets/SummaryField.d.ts +6 -0
- package/dist/fields/src/widgets/TimeField.d.ts +2 -0
- package/dist/fields/src/widgets/UserField.d.ts +6 -0
- package/dist/fields/src/widgets/VectorField.d.ts +6 -0
- package/dist/fields/src/widgets/types.d.ts +2 -0
- package/dist/index.cjs +4 -2
- package/dist/index.js +2003 -618
- package/package.json +10 -8
- package/src/complex-widgets.test.tsx +146 -0
- package/src/datetime-widgets.test.tsx +93 -0
- package/src/fields.stories.tsx +566 -0
- package/src/index.tsx +303 -68
- package/src/new-widgets.test.tsx +302 -0
- package/src/remaining-widgets.test.tsx +172 -0
- package/src/standard-widgets.test.tsx +278 -0
- package/src/text-rich-widgets.test.tsx +104 -0
- package/src/visual-widgets.test.tsx +97 -0
- package/src/widgets/AddressField.tsx +112 -0
- package/src/widgets/AutoNumberField.tsx +16 -0
- package/src/widgets/AvatarField.tsx +110 -0
- package/src/widgets/BooleanField.tsx +19 -11
- package/src/widgets/CodeField.tsx +34 -0
- package/src/widgets/ColorField.tsx +44 -0
- package/src/widgets/CurrencyField.tsx +8 -7
- package/src/widgets/DateField.tsx +5 -2
- package/src/widgets/DateTimeField.tsx +28 -0
- package/src/widgets/EmailField.tsx +7 -3
- package/src/widgets/FileField.tsx +114 -0
- package/src/widgets/FormulaField.tsx +31 -0
- package/src/widgets/GeolocationField.tsx +151 -0
- package/src/widgets/GridField.tsx +63 -0
- package/src/widgets/ImageField.tsx +108 -0
- package/src/widgets/LocationField.tsx +46 -0
- package/src/widgets/LookupField.tsx +5 -5
- package/src/widgets/MasterDetailField.tsx +131 -0
- package/src/widgets/NumberField.tsx +8 -4
- package/src/widgets/ObjectField.tsx +85 -0
- package/src/widgets/PasswordField.tsx +45 -0
- package/src/widgets/PercentField.tsx +49 -0
- package/src/widgets/PhoneField.tsx +7 -3
- package/src/widgets/QRCodeField.tsx +96 -0
- package/src/widgets/RatingField.tsx +66 -0
- package/src/widgets/RichTextField.tsx +5 -5
- package/src/widgets/SelectField.tsx +7 -5
- package/src/widgets/SignatureField.tsx +140 -0
- package/src/widgets/SliderField.tsx +39 -0
- package/src/widgets/SummaryField.tsx +29 -0
- package/src/widgets/TextAreaField.tsx +9 -6
- package/src/widgets/TextField.tsx +13 -8
- package/src/widgets/TimeField.tsx +22 -0
- package/src/widgets/UrlField.tsx +7 -3
- package/src/widgets/UserField.tsx +88 -0
- package/src/widgets/VectorField.tsx +28 -0
- 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
|
|
23
|
-
const format = richField
|
|
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={
|
|
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
|
|
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={
|
|
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
|
|
16
|
-
const maxLength = textareaField
|
|
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={
|
|
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 = (
|
|
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={
|
|
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
|
-
|
|
33
|
+
{...domProps}
|
|
34
|
+
type={fieldData?.type === 'password' ? 'password' : 'text'}
|
|
29
35
|
value={value || ''}
|
|
30
36
|
onChange={(e) => onChange(e.target.value)}
|
|
31
|
-
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
|
+
}
|
package/src/widgets/UrlField.tsx
CHANGED
|
@@ -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={
|
|
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
|
+
}
|
package/src/widgets/types.ts
CHANGED
|
@@ -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
|
}
|