@petrarca/sonnet-forms 0.1.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/LICENSE.md +190 -0
- package/dist/index.d.ts +1148 -0
- package/dist/index.js +3593 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3593 @@
|
|
|
1
|
+
// src/FormFieldWrapper.tsx
|
|
2
|
+
import { cn } from "@petrarca/sonnet-core";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
function FormFieldWrapper({
|
|
5
|
+
inputId,
|
|
6
|
+
label,
|
|
7
|
+
description,
|
|
8
|
+
error,
|
|
9
|
+
required,
|
|
10
|
+
compact,
|
|
11
|
+
className,
|
|
12
|
+
children
|
|
13
|
+
}) {
|
|
14
|
+
const showChrome = !compact;
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", { className: cn(showChrome && "space-y-2", className), children: [
|
|
16
|
+
showChrome && !!label && /* @__PURE__ */ jsxs(
|
|
17
|
+
"label",
|
|
18
|
+
{
|
|
19
|
+
htmlFor: inputId,
|
|
20
|
+
className: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
21
|
+
children: [
|
|
22
|
+
label,
|
|
23
|
+
required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
),
|
|
27
|
+
showChrome && description && /* @__PURE__ */ jsx(
|
|
28
|
+
"p",
|
|
29
|
+
{
|
|
30
|
+
id: `${inputId}-description`,
|
|
31
|
+
className: "text-sm text-muted-foreground",
|
|
32
|
+
children: description
|
|
33
|
+
}
|
|
34
|
+
),
|
|
35
|
+
children,
|
|
36
|
+
showChrome && error && /* @__PURE__ */ jsx(
|
|
37
|
+
"p",
|
|
38
|
+
{
|
|
39
|
+
id: `${inputId}-error`,
|
|
40
|
+
className: "text-sm font-medium text-destructive",
|
|
41
|
+
children: error
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
] });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/formFieldUtils.ts
|
|
48
|
+
function getAriaDescribedBy(inputId, error, description) {
|
|
49
|
+
if (error) return `${inputId}-error`;
|
|
50
|
+
if (description) return `${inputId}-description`;
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
function getAriaDescribedByCompact(inputId, error, description, compact) {
|
|
54
|
+
if (compact) return void 0;
|
|
55
|
+
return getAriaDescribedBy(inputId, error, description);
|
|
56
|
+
}
|
|
57
|
+
function getCompactAriaLabel(label, compact) {
|
|
58
|
+
if (compact && label) return String(label);
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
function getCompactTitle(error, compact) {
|
|
62
|
+
if (compact && error) return error;
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
function resolveFieldLabel(label) {
|
|
66
|
+
if (label === false) return void 0;
|
|
67
|
+
return label ?? void 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/FormInput.tsx
|
|
71
|
+
import React, { useId } from "react";
|
|
72
|
+
import { Input } from "@petrarca/sonnet-ui";
|
|
73
|
+
import { cn as cn2 } from "@petrarca/sonnet-core";
|
|
74
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
75
|
+
var FormInput = React.forwardRef(
|
|
76
|
+
({
|
|
77
|
+
label,
|
|
78
|
+
description,
|
|
79
|
+
error,
|
|
80
|
+
required,
|
|
81
|
+
wrapperClassName,
|
|
82
|
+
className,
|
|
83
|
+
id,
|
|
84
|
+
rightSection,
|
|
85
|
+
rightSectionWidth,
|
|
86
|
+
compact,
|
|
87
|
+
...props
|
|
88
|
+
}, ref) => {
|
|
89
|
+
const generatedId = useId();
|
|
90
|
+
const inputId = id ?? generatedId;
|
|
91
|
+
const paddingStyle = rightSectionWidth ? { paddingRight: `${rightSectionWidth}px` } : void 0;
|
|
92
|
+
return /* @__PURE__ */ jsx2(
|
|
93
|
+
FormFieldWrapper,
|
|
94
|
+
{
|
|
95
|
+
inputId,
|
|
96
|
+
label: label === false ? void 0 : label,
|
|
97
|
+
description,
|
|
98
|
+
error,
|
|
99
|
+
required,
|
|
100
|
+
compact,
|
|
101
|
+
className: wrapperClassName,
|
|
102
|
+
children: /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
|
|
103
|
+
/* @__PURE__ */ jsx2(
|
|
104
|
+
Input,
|
|
105
|
+
{
|
|
106
|
+
id: inputId,
|
|
107
|
+
ref,
|
|
108
|
+
required,
|
|
109
|
+
style: paddingStyle,
|
|
110
|
+
className: cn2(
|
|
111
|
+
error && "border-destructive focus-visible:ring-destructive",
|
|
112
|
+
className
|
|
113
|
+
),
|
|
114
|
+
"aria-invalid": error ? "true" : "false",
|
|
115
|
+
"aria-describedby": getAriaDescribedByCompact(
|
|
116
|
+
inputId,
|
|
117
|
+
error,
|
|
118
|
+
description,
|
|
119
|
+
compact
|
|
120
|
+
),
|
|
121
|
+
"aria-label": getCompactAriaLabel(label, compact),
|
|
122
|
+
title: getCompactTitle(error, compact),
|
|
123
|
+
...props
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
rightSection && /* @__PURE__ */ jsx2("div", { className: "absolute top-1/2 -translate-y-1/2 right-2 flex items-center gap-1", children: rightSection })
|
|
127
|
+
] })
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
FormInput.displayName = "FormInput";
|
|
133
|
+
|
|
134
|
+
// src/FormTextarea.tsx
|
|
135
|
+
import React2, { useEffect, useId as useId2, useRef } from "react";
|
|
136
|
+
import { Textarea } from "@petrarca/sonnet-ui";
|
|
137
|
+
import { cn as cn3 } from "@petrarca/sonnet-core";
|
|
138
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
139
|
+
function useMergedRef(forwardedRef, internalRef) {
|
|
140
|
+
return (element) => {
|
|
141
|
+
internalRef.current = element;
|
|
142
|
+
if (typeof forwardedRef === "function") {
|
|
143
|
+
forwardedRef(element);
|
|
144
|
+
} else if (forwardedRef) {
|
|
145
|
+
forwardedRef.current = element;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function useAutoResize(ref, value, autosize, minRows) {
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!autosize || !ref.current) return;
|
|
152
|
+
const textarea = ref.current;
|
|
153
|
+
textarea.style.height = "auto";
|
|
154
|
+
const newHeight = Math.max(textarea.scrollHeight, minRows * 24);
|
|
155
|
+
textarea.style.height = `${newHeight}px`;
|
|
156
|
+
}, [value, autosize, minRows, ref]);
|
|
157
|
+
}
|
|
158
|
+
function textareaClassName(error, autosize, className) {
|
|
159
|
+
return cn3(
|
|
160
|
+
error && "border-destructive focus-visible:ring-destructive",
|
|
161
|
+
autosize && "resize-none overflow-hidden",
|
|
162
|
+
"h-full",
|
|
163
|
+
className
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
var FormTextarea = React2.forwardRef((allProps, ref) => {
|
|
167
|
+
const {
|
|
168
|
+
label,
|
|
169
|
+
description,
|
|
170
|
+
error,
|
|
171
|
+
required,
|
|
172
|
+
wrapperClassName,
|
|
173
|
+
className,
|
|
174
|
+
id,
|
|
175
|
+
value,
|
|
176
|
+
onChange,
|
|
177
|
+
rightSection,
|
|
178
|
+
rightSectionWidth,
|
|
179
|
+
...props
|
|
180
|
+
} = allProps;
|
|
181
|
+
const autosize = allProps.autosize ?? false;
|
|
182
|
+
const minRows = allProps.minRows ?? 3;
|
|
183
|
+
const generatedId = useId2();
|
|
184
|
+
const textareaId = id ?? generatedId;
|
|
185
|
+
const internalRef = useRef(null);
|
|
186
|
+
const setRefs = useMergedRef(ref, internalRef);
|
|
187
|
+
useAutoResize(internalRef, value, autosize, minRows);
|
|
188
|
+
const paddingStyle = rightSectionWidth ? { paddingRight: `${rightSectionWidth}px` } : void 0;
|
|
189
|
+
const effectiveRows = autosize ? minRows : props.rows;
|
|
190
|
+
const resolvedLabel = label === false ? void 0 : label;
|
|
191
|
+
return /* @__PURE__ */ jsx3(
|
|
192
|
+
FormFieldWrapper,
|
|
193
|
+
{
|
|
194
|
+
inputId: textareaId,
|
|
195
|
+
label: resolvedLabel,
|
|
196
|
+
description,
|
|
197
|
+
error,
|
|
198
|
+
required,
|
|
199
|
+
className: cn3("flex flex-col", wrapperClassName),
|
|
200
|
+
children: /* @__PURE__ */ jsxs3("div", { className: "relative flex-1 min-h-0", children: [
|
|
201
|
+
/* @__PURE__ */ jsx3(
|
|
202
|
+
Textarea,
|
|
203
|
+
{
|
|
204
|
+
id: textareaId,
|
|
205
|
+
ref: setRefs,
|
|
206
|
+
value,
|
|
207
|
+
onChange,
|
|
208
|
+
required,
|
|
209
|
+
rows: effectiveRows,
|
|
210
|
+
className: textareaClassName(error, autosize, className),
|
|
211
|
+
style: paddingStyle,
|
|
212
|
+
"aria-invalid": error ? "true" : "false",
|
|
213
|
+
"aria-describedby": getAriaDescribedBy(textareaId, error, description),
|
|
214
|
+
...props
|
|
215
|
+
}
|
|
216
|
+
),
|
|
217
|
+
rightSection && /* @__PURE__ */ jsx3("div", { className: "absolute top-2 right-2 flex items-start gap-1", children: rightSection })
|
|
218
|
+
] })
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
FormTextarea.displayName = "FormTextarea";
|
|
223
|
+
|
|
224
|
+
// src/FormNumberInput.tsx
|
|
225
|
+
import React3, { useId as useId3 } from "react";
|
|
226
|
+
import { Input as Input2 } from "@petrarca/sonnet-ui";
|
|
227
|
+
import { cn as cn4 } from "@petrarca/sonnet-core";
|
|
228
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
229
|
+
var FormNumberInput = React3.forwardRef(
|
|
230
|
+
({
|
|
231
|
+
value,
|
|
232
|
+
label,
|
|
233
|
+
description,
|
|
234
|
+
error,
|
|
235
|
+
placeholder,
|
|
236
|
+
required,
|
|
237
|
+
disabled,
|
|
238
|
+
readOnly,
|
|
239
|
+
min,
|
|
240
|
+
max,
|
|
241
|
+
step,
|
|
242
|
+
allowDecimal = true,
|
|
243
|
+
onChange,
|
|
244
|
+
onBlur,
|
|
245
|
+
autoFocus,
|
|
246
|
+
wrapperClassName,
|
|
247
|
+
className,
|
|
248
|
+
id,
|
|
249
|
+
name
|
|
250
|
+
}, ref) => {
|
|
251
|
+
const generatedId = useId3();
|
|
252
|
+
const inputId = id ?? generatedId;
|
|
253
|
+
const handleChange = (e) => {
|
|
254
|
+
if (!onChange) return;
|
|
255
|
+
const val = e.target.value;
|
|
256
|
+
if (val === "" || val === void 0) {
|
|
257
|
+
onChange(void 0);
|
|
258
|
+
} else {
|
|
259
|
+
const numVal = parseFloat(val);
|
|
260
|
+
if (!isNaN(numVal)) onChange(numVal);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const displayValue = value !== void 0 && value !== null ? String(value) : "";
|
|
264
|
+
return /* @__PURE__ */ jsx4(
|
|
265
|
+
FormFieldWrapper,
|
|
266
|
+
{
|
|
267
|
+
inputId,
|
|
268
|
+
label,
|
|
269
|
+
description,
|
|
270
|
+
error,
|
|
271
|
+
required,
|
|
272
|
+
className: wrapperClassName,
|
|
273
|
+
children: /* @__PURE__ */ jsx4(
|
|
274
|
+
Input2,
|
|
275
|
+
{
|
|
276
|
+
id: inputId,
|
|
277
|
+
name,
|
|
278
|
+
ref,
|
|
279
|
+
type: "number",
|
|
280
|
+
inputMode: "numeric",
|
|
281
|
+
value: displayValue,
|
|
282
|
+
onChange: handleChange,
|
|
283
|
+
onBlur,
|
|
284
|
+
placeholder,
|
|
285
|
+
disabled,
|
|
286
|
+
readOnly,
|
|
287
|
+
autoFocus,
|
|
288
|
+
min,
|
|
289
|
+
max,
|
|
290
|
+
step: step ?? (allowDecimal ? "any" : 1),
|
|
291
|
+
className: cn4(error && "border-destructive", className),
|
|
292
|
+
"aria-invalid": error ? "true" : "false",
|
|
293
|
+
"aria-describedby": getAriaDescribedBy(inputId, error, description),
|
|
294
|
+
"aria-required": required
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
FormNumberInput.displayName = "FormNumberInput";
|
|
302
|
+
|
|
303
|
+
// src/FormCheckbox.tsx
|
|
304
|
+
import React4, { useId as useId4 } from "react";
|
|
305
|
+
import { Checkbox } from "@petrarca/sonnet-ui";
|
|
306
|
+
import { cn as cn5 } from "@petrarca/sonnet-core";
|
|
307
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
308
|
+
function CheckboxLabel({
|
|
309
|
+
htmlFor,
|
|
310
|
+
label,
|
|
311
|
+
required,
|
|
312
|
+
disabled,
|
|
313
|
+
compact
|
|
314
|
+
}) {
|
|
315
|
+
if (compact || !label) return null;
|
|
316
|
+
return /* @__PURE__ */ jsxs4(
|
|
317
|
+
"label",
|
|
318
|
+
{
|
|
319
|
+
htmlFor,
|
|
320
|
+
className: cn5(
|
|
321
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
322
|
+
disabled && "opacity-70 cursor-not-allowed"
|
|
323
|
+
),
|
|
324
|
+
children: [
|
|
325
|
+
label,
|
|
326
|
+
required && /* @__PURE__ */ jsx5("span", { className: "text-destructive ml-1", children: "*" })
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
function CheckboxChrome({
|
|
332
|
+
inputId,
|
|
333
|
+
description,
|
|
334
|
+
error,
|
|
335
|
+
compact
|
|
336
|
+
}) {
|
|
337
|
+
if (compact) return null;
|
|
338
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
339
|
+
description && /* @__PURE__ */ jsx5(
|
|
340
|
+
"p",
|
|
341
|
+
{
|
|
342
|
+
id: `${inputId}-description`,
|
|
343
|
+
className: "text-sm text-muted-foreground ml-6",
|
|
344
|
+
children: description
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
error && /* @__PURE__ */ jsx5("p", { id: `${inputId}-error`, className: "text-sm text-destructive ml-6", children: error })
|
|
348
|
+
] });
|
|
349
|
+
}
|
|
350
|
+
var FormCheckbox = React4.forwardRef(
|
|
351
|
+
({
|
|
352
|
+
checked,
|
|
353
|
+
label,
|
|
354
|
+
description,
|
|
355
|
+
error,
|
|
356
|
+
required,
|
|
357
|
+
disabled,
|
|
358
|
+
readOnly,
|
|
359
|
+
onChange,
|
|
360
|
+
onBlur,
|
|
361
|
+
autoFocus,
|
|
362
|
+
wrapperClassName,
|
|
363
|
+
className,
|
|
364
|
+
id,
|
|
365
|
+
compact
|
|
366
|
+
}, ref) => {
|
|
367
|
+
const generatedId = useId4();
|
|
368
|
+
const checkboxId = id ?? generatedId;
|
|
369
|
+
const handleCheckedChange = (newChecked) => {
|
|
370
|
+
if (!readOnly && onChange) {
|
|
371
|
+
onChange(newChecked);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
return /* @__PURE__ */ jsxs4("div", { className: cn5(!compact && "space-y-2", wrapperClassName), children: [
|
|
375
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-start space-x-2", children: [
|
|
376
|
+
/* @__PURE__ */ jsx5(
|
|
377
|
+
Checkbox,
|
|
378
|
+
{
|
|
379
|
+
id: checkboxId,
|
|
380
|
+
ref,
|
|
381
|
+
checked,
|
|
382
|
+
onCheckedChange: handleCheckedChange,
|
|
383
|
+
onBlur,
|
|
384
|
+
disabled: disabled || readOnly,
|
|
385
|
+
autoFocus,
|
|
386
|
+
className: cn5(error && "border-destructive", className),
|
|
387
|
+
"aria-invalid": error ? "true" : "false",
|
|
388
|
+
"aria-describedby": getAriaDescribedByCompact(
|
|
389
|
+
checkboxId,
|
|
390
|
+
error,
|
|
391
|
+
description,
|
|
392
|
+
compact
|
|
393
|
+
),
|
|
394
|
+
"aria-label": getCompactAriaLabel(label, compact),
|
|
395
|
+
title: getCompactTitle(error, compact)
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
/* @__PURE__ */ jsx5(
|
|
399
|
+
CheckboxLabel,
|
|
400
|
+
{
|
|
401
|
+
htmlFor: checkboxId,
|
|
402
|
+
label,
|
|
403
|
+
required,
|
|
404
|
+
disabled,
|
|
405
|
+
compact
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
] }),
|
|
409
|
+
/* @__PURE__ */ jsx5(
|
|
410
|
+
CheckboxChrome,
|
|
411
|
+
{
|
|
412
|
+
inputId: checkboxId,
|
|
413
|
+
description,
|
|
414
|
+
error,
|
|
415
|
+
compact
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
] });
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
FormCheckbox.displayName = "FormCheckbox";
|
|
422
|
+
|
|
423
|
+
// src/FormSelect.tsx
|
|
424
|
+
import * as React5 from "react";
|
|
425
|
+
import { Check, ChevronsUpDown, X } from "lucide-react";
|
|
426
|
+
import { cn as cn6 } from "@petrarca/sonnet-core";
|
|
427
|
+
import { Button } from "@petrarca/sonnet-ui";
|
|
428
|
+
import {
|
|
429
|
+
Command,
|
|
430
|
+
CommandEmpty,
|
|
431
|
+
CommandGroup,
|
|
432
|
+
CommandInput,
|
|
433
|
+
CommandItem,
|
|
434
|
+
CommandList
|
|
435
|
+
} from "@petrarca/sonnet-ui";
|
|
436
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@petrarca/sonnet-ui";
|
|
437
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
438
|
+
function TriggerButton({
|
|
439
|
+
selectId,
|
|
440
|
+
open,
|
|
441
|
+
selectedOption,
|
|
442
|
+
placeholder,
|
|
443
|
+
error,
|
|
444
|
+
compact,
|
|
445
|
+
label,
|
|
446
|
+
description,
|
|
447
|
+
clearable,
|
|
448
|
+
value,
|
|
449
|
+
isDisabled,
|
|
450
|
+
autoFocus,
|
|
451
|
+
className,
|
|
452
|
+
onClear
|
|
453
|
+
}) {
|
|
454
|
+
return /* @__PURE__ */ jsxs5(
|
|
455
|
+
Button,
|
|
456
|
+
{
|
|
457
|
+
id: selectId,
|
|
458
|
+
variant: "outline",
|
|
459
|
+
role: "combobox",
|
|
460
|
+
"aria-expanded": open,
|
|
461
|
+
"aria-invalid": error ? "true" : "false",
|
|
462
|
+
"aria-describedby": getAriaDescribedByCompact(
|
|
463
|
+
selectId,
|
|
464
|
+
error,
|
|
465
|
+
description,
|
|
466
|
+
compact
|
|
467
|
+
),
|
|
468
|
+
"aria-label": getCompactAriaLabel(label, compact),
|
|
469
|
+
title: getCompactTitle(error, compact),
|
|
470
|
+
className: cn6(
|
|
471
|
+
"w-full justify-between",
|
|
472
|
+
!selectedOption && "text-muted-foreground",
|
|
473
|
+
error && "border-destructive",
|
|
474
|
+
className
|
|
475
|
+
),
|
|
476
|
+
disabled: isDisabled,
|
|
477
|
+
autoFocus,
|
|
478
|
+
children: [
|
|
479
|
+
/* @__PURE__ */ jsx6("span", { className: "truncate", children: selectedOption ? selectedOption.label : placeholder }),
|
|
480
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1", children: [
|
|
481
|
+
clearable && value && !isDisabled && /* @__PURE__ */ jsx6(
|
|
482
|
+
X,
|
|
483
|
+
{
|
|
484
|
+
className: "h-4 w-4 opacity-50 hover:opacity-100",
|
|
485
|
+
onClick: onClear
|
|
486
|
+
}
|
|
487
|
+
),
|
|
488
|
+
/* @__PURE__ */ jsx6(ChevronsUpDown, { className: "h-4 w-4 shrink-0 opacity-50" })
|
|
489
|
+
] })
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
function SelectDropdown({
|
|
495
|
+
searchPlaceholder,
|
|
496
|
+
searchQuery,
|
|
497
|
+
onSearchChange,
|
|
498
|
+
loading,
|
|
499
|
+
emptyMessage,
|
|
500
|
+
options,
|
|
501
|
+
value,
|
|
502
|
+
onSelect
|
|
503
|
+
}) {
|
|
504
|
+
return /* @__PURE__ */ jsxs5(Command, { shouldFilter: false, children: [
|
|
505
|
+
/* @__PURE__ */ jsx6(
|
|
506
|
+
CommandInput,
|
|
507
|
+
{
|
|
508
|
+
placeholder: searchPlaceholder,
|
|
509
|
+
value: searchQuery,
|
|
510
|
+
onValueChange: onSearchChange
|
|
511
|
+
}
|
|
512
|
+
),
|
|
513
|
+
/* @__PURE__ */ jsxs5(CommandList, { children: [
|
|
514
|
+
/* @__PURE__ */ jsx6(CommandEmpty, { children: loading ? "Loading..." : emptyMessage }),
|
|
515
|
+
/* @__PURE__ */ jsx6(CommandGroup, { children: options.filter(
|
|
516
|
+
(option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
|
517
|
+
).map((option) => /* @__PURE__ */ jsxs5(
|
|
518
|
+
CommandItem,
|
|
519
|
+
{
|
|
520
|
+
value: option.value,
|
|
521
|
+
onSelect,
|
|
522
|
+
children: [
|
|
523
|
+
/* @__PURE__ */ jsx6(
|
|
524
|
+
Check,
|
|
525
|
+
{
|
|
526
|
+
className: cn6(
|
|
527
|
+
"mr-2 h-4 w-4",
|
|
528
|
+
value === option.value ? "opacity-100" : "opacity-0"
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
),
|
|
532
|
+
option.label
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
option.value
|
|
536
|
+
)) })
|
|
537
|
+
] })
|
|
538
|
+
] });
|
|
539
|
+
}
|
|
540
|
+
function useSelectState(value, onChange, onBlur) {
|
|
541
|
+
const [open, setOpen] = React5.useState(false);
|
|
542
|
+
const [searchQuery, setSearchQuery] = React5.useState("");
|
|
543
|
+
const handleSelect = (selectedValue) => {
|
|
544
|
+
onChange?.(selectedValue === value ? null : selectedValue);
|
|
545
|
+
setOpen(false);
|
|
546
|
+
setSearchQuery("");
|
|
547
|
+
};
|
|
548
|
+
const handleClear = (e) => {
|
|
549
|
+
e.stopPropagation();
|
|
550
|
+
onChange?.(null);
|
|
551
|
+
};
|
|
552
|
+
const handleOpenChange = (newOpen) => {
|
|
553
|
+
setOpen(newOpen);
|
|
554
|
+
if (newOpen) return;
|
|
555
|
+
setSearchQuery("");
|
|
556
|
+
onBlur?.();
|
|
557
|
+
};
|
|
558
|
+
return {
|
|
559
|
+
open,
|
|
560
|
+
searchQuery,
|
|
561
|
+
setSearchQuery,
|
|
562
|
+
handleSelect,
|
|
563
|
+
handleClear,
|
|
564
|
+
handleOpenChange
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function resolveSelectDefaults(props) {
|
|
568
|
+
return {
|
|
569
|
+
placeholder: props.placeholder ?? "Select...",
|
|
570
|
+
searchPlaceholder: props.searchPlaceholder ?? "Search...",
|
|
571
|
+
emptyMessage: props.emptyMessage ?? "No results found",
|
|
572
|
+
clearable: props.clearable ?? true,
|
|
573
|
+
disabled: props.disabled ?? false,
|
|
574
|
+
readOnly: props.readOnly ?? false,
|
|
575
|
+
loading: props.loading ?? false
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function FormSelect(props) {
|
|
579
|
+
const {
|
|
580
|
+
label,
|
|
581
|
+
description,
|
|
582
|
+
error,
|
|
583
|
+
required,
|
|
584
|
+
wrapperClassName,
|
|
585
|
+
compact,
|
|
586
|
+
options,
|
|
587
|
+
value,
|
|
588
|
+
onChange,
|
|
589
|
+
onBlur,
|
|
590
|
+
className,
|
|
591
|
+
id,
|
|
592
|
+
autoFocus
|
|
593
|
+
} = props;
|
|
594
|
+
const {
|
|
595
|
+
placeholder,
|
|
596
|
+
searchPlaceholder,
|
|
597
|
+
emptyMessage,
|
|
598
|
+
clearable,
|
|
599
|
+
disabled,
|
|
600
|
+
readOnly,
|
|
601
|
+
loading
|
|
602
|
+
} = resolveSelectDefaults(props);
|
|
603
|
+
const generatedId = React5.useId();
|
|
604
|
+
const selectId = id ?? generatedId;
|
|
605
|
+
const {
|
|
606
|
+
open,
|
|
607
|
+
searchQuery,
|
|
608
|
+
setSearchQuery,
|
|
609
|
+
handleSelect,
|
|
610
|
+
handleClear,
|
|
611
|
+
handleOpenChange
|
|
612
|
+
} = useSelectState(value, onChange, onBlur);
|
|
613
|
+
const selectedOption = options.find((opt) => opt.value === value);
|
|
614
|
+
const isDisabled = disabled || readOnly || loading;
|
|
615
|
+
return /* @__PURE__ */ jsx6(
|
|
616
|
+
FormFieldWrapper,
|
|
617
|
+
{
|
|
618
|
+
inputId: selectId,
|
|
619
|
+
label,
|
|
620
|
+
description,
|
|
621
|
+
error,
|
|
622
|
+
required,
|
|
623
|
+
compact,
|
|
624
|
+
className: wrapperClassName,
|
|
625
|
+
children: /* @__PURE__ */ jsxs5(Popover, { open, onOpenChange: handleOpenChange, children: [
|
|
626
|
+
/* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx6(
|
|
627
|
+
TriggerButton,
|
|
628
|
+
{
|
|
629
|
+
selectId,
|
|
630
|
+
open,
|
|
631
|
+
selectedOption,
|
|
632
|
+
placeholder,
|
|
633
|
+
error,
|
|
634
|
+
compact,
|
|
635
|
+
label,
|
|
636
|
+
description,
|
|
637
|
+
clearable,
|
|
638
|
+
value,
|
|
639
|
+
isDisabled,
|
|
640
|
+
autoFocus,
|
|
641
|
+
className,
|
|
642
|
+
onClear: handleClear
|
|
643
|
+
}
|
|
644
|
+
) }),
|
|
645
|
+
/* @__PURE__ */ jsx6(
|
|
646
|
+
PopoverContent,
|
|
647
|
+
{
|
|
648
|
+
className: "w-[--radix-popover-trigger-width] p-0",
|
|
649
|
+
align: "start",
|
|
650
|
+
children: /* @__PURE__ */ jsx6(
|
|
651
|
+
SelectDropdown,
|
|
652
|
+
{
|
|
653
|
+
searchPlaceholder,
|
|
654
|
+
searchQuery,
|
|
655
|
+
onSearchChange: setSearchQuery,
|
|
656
|
+
loading,
|
|
657
|
+
emptyMessage,
|
|
658
|
+
options,
|
|
659
|
+
value,
|
|
660
|
+
onSelect: handleSelect
|
|
661
|
+
}
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
] })
|
|
666
|
+
}
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/FormMultiSelect.tsx
|
|
671
|
+
import * as React6 from "react";
|
|
672
|
+
import { Check as Check2, ChevronsUpDown as ChevronsUpDown2, X as X2 } from "lucide-react";
|
|
673
|
+
import { cn as cn7 } from "@petrarca/sonnet-core";
|
|
674
|
+
import { Button as Button2 } from "@petrarca/sonnet-ui";
|
|
675
|
+
import { Badge } from "@petrarca/sonnet-ui";
|
|
676
|
+
import {
|
|
677
|
+
Command as Command2,
|
|
678
|
+
CommandEmpty as CommandEmpty2,
|
|
679
|
+
CommandGroup as CommandGroup2,
|
|
680
|
+
CommandInput as CommandInput2,
|
|
681
|
+
CommandItem as CommandItem2,
|
|
682
|
+
CommandList as CommandList2
|
|
683
|
+
} from "@petrarca/sonnet-ui";
|
|
684
|
+
import { Popover as Popover2, PopoverContent as PopoverContent2, PopoverTrigger as PopoverTrigger2 } from "@petrarca/sonnet-ui";
|
|
685
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
686
|
+
function RemovableBadge({
|
|
687
|
+
option,
|
|
688
|
+
isDisabled,
|
|
689
|
+
badgeColor,
|
|
690
|
+
onRemove
|
|
691
|
+
}) {
|
|
692
|
+
return /* @__PURE__ */ jsxs6(Badge, { variant: "secondary", badgeColor, className: "mr-1", children: [
|
|
693
|
+
option.label,
|
|
694
|
+
!isDisabled && /* @__PURE__ */ jsx7(
|
|
695
|
+
"button",
|
|
696
|
+
{
|
|
697
|
+
type: "button",
|
|
698
|
+
className: "ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
699
|
+
onKeyDown: (e) => {
|
|
700
|
+
if (e.key === "Enter") onRemove(option.value, e);
|
|
701
|
+
},
|
|
702
|
+
onMouseDown: (e) => {
|
|
703
|
+
e.preventDefault();
|
|
704
|
+
e.stopPropagation();
|
|
705
|
+
},
|
|
706
|
+
onClick: (e) => onRemove(option.value, e),
|
|
707
|
+
children: /* @__PURE__ */ jsx7(X2, { className: "h-3 w-3 text-muted-foreground hover:text-foreground" })
|
|
708
|
+
}
|
|
709
|
+
)
|
|
710
|
+
] });
|
|
711
|
+
}
|
|
712
|
+
function SelectedBadges({
|
|
713
|
+
selectedOptions,
|
|
714
|
+
effectiveMaxDisplay,
|
|
715
|
+
expanded,
|
|
716
|
+
maxDisplay,
|
|
717
|
+
isDisabled,
|
|
718
|
+
badgeColor,
|
|
719
|
+
placeholder,
|
|
720
|
+
onRemove,
|
|
721
|
+
onExpand,
|
|
722
|
+
onCollapse
|
|
723
|
+
}) {
|
|
724
|
+
if (selectedOptions.length === 0) {
|
|
725
|
+
return /* @__PURE__ */ jsx7("span", { className: "text-muted-foreground", children: placeholder });
|
|
726
|
+
}
|
|
727
|
+
const displayedBadges = selectedOptions.slice(0, effectiveMaxDisplay);
|
|
728
|
+
const remainingCount = selectedOptions.length - effectiveMaxDisplay;
|
|
729
|
+
const showCollapse = expanded && remainingCount <= 0 && selectedOptions.length > maxDisplay;
|
|
730
|
+
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
731
|
+
displayedBadges.map((option) => /* @__PURE__ */ jsx7(
|
|
732
|
+
RemovableBadge,
|
|
733
|
+
{
|
|
734
|
+
option,
|
|
735
|
+
isDisabled,
|
|
736
|
+
badgeColor,
|
|
737
|
+
onRemove
|
|
738
|
+
},
|
|
739
|
+
option.value
|
|
740
|
+
)),
|
|
741
|
+
remainingCount > 0 && /* @__PURE__ */ jsxs6(
|
|
742
|
+
Badge,
|
|
743
|
+
{
|
|
744
|
+
variant: "secondary",
|
|
745
|
+
badgeColor,
|
|
746
|
+
className: "mr-1 cursor-pointer hover:bg-secondary/80",
|
|
747
|
+
onClick: onExpand,
|
|
748
|
+
children: [
|
|
749
|
+
"+",
|
|
750
|
+
remainingCount,
|
|
751
|
+
" more"
|
|
752
|
+
]
|
|
753
|
+
}
|
|
754
|
+
),
|
|
755
|
+
showCollapse && /* @__PURE__ */ jsx7(
|
|
756
|
+
Badge,
|
|
757
|
+
{
|
|
758
|
+
variant: "secondary",
|
|
759
|
+
badgeColor,
|
|
760
|
+
className: "mr-1 cursor-pointer hover:bg-secondary/80",
|
|
761
|
+
onClick: onCollapse,
|
|
762
|
+
children: "Show less"
|
|
763
|
+
}
|
|
764
|
+
)
|
|
765
|
+
] });
|
|
766
|
+
}
|
|
767
|
+
function MultiSelectDropdown({
|
|
768
|
+
searchPlaceholder,
|
|
769
|
+
searchQuery,
|
|
770
|
+
onSearchChange,
|
|
771
|
+
loading,
|
|
772
|
+
emptyMessage,
|
|
773
|
+
options,
|
|
774
|
+
value,
|
|
775
|
+
onSelect
|
|
776
|
+
}) {
|
|
777
|
+
return /* @__PURE__ */ jsxs6(Command2, { shouldFilter: false, children: [
|
|
778
|
+
/* @__PURE__ */ jsx7(
|
|
779
|
+
CommandInput2,
|
|
780
|
+
{
|
|
781
|
+
placeholder: searchPlaceholder,
|
|
782
|
+
value: searchQuery,
|
|
783
|
+
onValueChange: onSearchChange
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
/* @__PURE__ */ jsxs6(CommandList2, { children: [
|
|
787
|
+
/* @__PURE__ */ jsx7(CommandEmpty2, { children: loading ? "Loading..." : emptyMessage }),
|
|
788
|
+
/* @__PURE__ */ jsx7(CommandGroup2, { children: options.filter(
|
|
789
|
+
(option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
|
790
|
+
).map((option) => /* @__PURE__ */ jsxs6(
|
|
791
|
+
CommandItem2,
|
|
792
|
+
{
|
|
793
|
+
value: option.value,
|
|
794
|
+
onSelect,
|
|
795
|
+
children: [
|
|
796
|
+
/* @__PURE__ */ jsx7(
|
|
797
|
+
Check2,
|
|
798
|
+
{
|
|
799
|
+
className: cn7(
|
|
800
|
+
"mr-2 h-4 w-4",
|
|
801
|
+
value.includes(option.value) ? "opacity-100" : "opacity-0"
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
),
|
|
805
|
+
option.label
|
|
806
|
+
]
|
|
807
|
+
},
|
|
808
|
+
option.value
|
|
809
|
+
)) })
|
|
810
|
+
] })
|
|
811
|
+
] });
|
|
812
|
+
}
|
|
813
|
+
function useMultiSelectState(value, onChange, onBlur) {
|
|
814
|
+
const [open, setOpen] = React6.useState(false);
|
|
815
|
+
const [searchQuery, setSearchQuery] = React6.useState("");
|
|
816
|
+
const [expanded, setExpanded] = React6.useState(false);
|
|
817
|
+
const handleSelect = (selectedValue) => {
|
|
818
|
+
const newValue = value.includes(selectedValue) ? value.filter((v) => v !== selectedValue) : [...value, selectedValue];
|
|
819
|
+
onChange?.(newValue);
|
|
820
|
+
};
|
|
821
|
+
const handleRemove = (valueToRemove, e) => {
|
|
822
|
+
e.stopPropagation();
|
|
823
|
+
onChange?.(value.filter((v) => v !== valueToRemove));
|
|
824
|
+
};
|
|
825
|
+
const handleClearAll = (e) => {
|
|
826
|
+
e.stopPropagation();
|
|
827
|
+
onChange?.([]);
|
|
828
|
+
};
|
|
829
|
+
const handleOpenChange = (newOpen) => {
|
|
830
|
+
setOpen(newOpen);
|
|
831
|
+
if (newOpen) return;
|
|
832
|
+
setSearchQuery("");
|
|
833
|
+
setExpanded(false);
|
|
834
|
+
onBlur?.();
|
|
835
|
+
};
|
|
836
|
+
const handleExpand = (e) => {
|
|
837
|
+
e.stopPropagation();
|
|
838
|
+
setExpanded(true);
|
|
839
|
+
};
|
|
840
|
+
const handleCollapse = (e) => {
|
|
841
|
+
e.stopPropagation();
|
|
842
|
+
setExpanded(false);
|
|
843
|
+
};
|
|
844
|
+
return {
|
|
845
|
+
open,
|
|
846
|
+
searchQuery,
|
|
847
|
+
setSearchQuery,
|
|
848
|
+
expanded,
|
|
849
|
+
handleSelect,
|
|
850
|
+
handleRemove,
|
|
851
|
+
handleClearAll,
|
|
852
|
+
handleOpenChange,
|
|
853
|
+
handleExpand,
|
|
854
|
+
handleCollapse
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function resolveMultiSelectDefaults(props) {
|
|
858
|
+
return {
|
|
859
|
+
placeholder: props.placeholder ?? "Select...",
|
|
860
|
+
searchPlaceholder: props.searchPlaceholder ?? "Search...",
|
|
861
|
+
emptyMessage: props.emptyMessage ?? "No results found",
|
|
862
|
+
clearable: props.clearable ?? true,
|
|
863
|
+
disabled: props.disabled ?? false,
|
|
864
|
+
readOnly: props.readOnly ?? false,
|
|
865
|
+
loading: props.loading ?? false,
|
|
866
|
+
maxDisplay: props.maxDisplay ?? Infinity
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function MultiSelectTrigger({
|
|
870
|
+
id,
|
|
871
|
+
open,
|
|
872
|
+
error,
|
|
873
|
+
className,
|
|
874
|
+
isDisabled,
|
|
875
|
+
autoFocus,
|
|
876
|
+
clearable,
|
|
877
|
+
hasValues,
|
|
878
|
+
selectedOptions,
|
|
879
|
+
effectiveMaxDisplay,
|
|
880
|
+
expanded,
|
|
881
|
+
maxDisplay,
|
|
882
|
+
badgeColor,
|
|
883
|
+
placeholder,
|
|
884
|
+
onRemove,
|
|
885
|
+
onExpand,
|
|
886
|
+
onCollapse,
|
|
887
|
+
onClearAll
|
|
888
|
+
}) {
|
|
889
|
+
return /* @__PURE__ */ jsxs6(
|
|
890
|
+
Button2,
|
|
891
|
+
{
|
|
892
|
+
id,
|
|
893
|
+
variant: "outline",
|
|
894
|
+
role: "combobox",
|
|
895
|
+
"aria-expanded": open,
|
|
896
|
+
className: cn7(
|
|
897
|
+
"w-full justify-between min-h-10 h-auto",
|
|
898
|
+
error && "border-destructive",
|
|
899
|
+
className
|
|
900
|
+
),
|
|
901
|
+
disabled: isDisabled,
|
|
902
|
+
autoFocus,
|
|
903
|
+
children: [
|
|
904
|
+
/* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-1 flex-1", children: /* @__PURE__ */ jsx7(
|
|
905
|
+
SelectedBadges,
|
|
906
|
+
{
|
|
907
|
+
selectedOptions,
|
|
908
|
+
effectiveMaxDisplay,
|
|
909
|
+
expanded,
|
|
910
|
+
maxDisplay,
|
|
911
|
+
isDisabled,
|
|
912
|
+
badgeColor,
|
|
913
|
+
placeholder,
|
|
914
|
+
onRemove,
|
|
915
|
+
onExpand,
|
|
916
|
+
onCollapse
|
|
917
|
+
}
|
|
918
|
+
) }),
|
|
919
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1 ml-2", children: [
|
|
920
|
+
clearable && hasValues && !isDisabled && /* @__PURE__ */ jsx7(
|
|
921
|
+
X2,
|
|
922
|
+
{
|
|
923
|
+
className: "h-4 w-4 opacity-50 hover:opacity-100",
|
|
924
|
+
onClick: onClearAll
|
|
925
|
+
}
|
|
926
|
+
),
|
|
927
|
+
/* @__PURE__ */ jsx7(ChevronsUpDown2, { className: "h-4 w-4 shrink-0 opacity-50" })
|
|
928
|
+
] })
|
|
929
|
+
]
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
function FormMultiSelect(props) {
|
|
934
|
+
const {
|
|
935
|
+
label,
|
|
936
|
+
description,
|
|
937
|
+
error,
|
|
938
|
+
required,
|
|
939
|
+
wrapperClassName,
|
|
940
|
+
options,
|
|
941
|
+
onChange,
|
|
942
|
+
onBlur,
|
|
943
|
+
className,
|
|
944
|
+
id,
|
|
945
|
+
name,
|
|
946
|
+
autoFocus,
|
|
947
|
+
badgeColor
|
|
948
|
+
} = props;
|
|
949
|
+
const value = props.value ?? [];
|
|
950
|
+
const generatedId = React6.useId();
|
|
951
|
+
const fieldId = id ?? generatedId;
|
|
952
|
+
const {
|
|
953
|
+
placeholder,
|
|
954
|
+
searchPlaceholder,
|
|
955
|
+
emptyMessage,
|
|
956
|
+
clearable,
|
|
957
|
+
disabled,
|
|
958
|
+
readOnly,
|
|
959
|
+
loading,
|
|
960
|
+
maxDisplay
|
|
961
|
+
} = resolveMultiSelectDefaults(props);
|
|
962
|
+
const selectedOptions = options.filter((opt) => value.includes(opt.value));
|
|
963
|
+
const isDisabled = disabled || readOnly || loading;
|
|
964
|
+
const {
|
|
965
|
+
open,
|
|
966
|
+
searchQuery,
|
|
967
|
+
setSearchQuery,
|
|
968
|
+
expanded,
|
|
969
|
+
handleSelect,
|
|
970
|
+
handleRemove,
|
|
971
|
+
handleClearAll,
|
|
972
|
+
handleOpenChange,
|
|
973
|
+
handleExpand,
|
|
974
|
+
handleCollapse
|
|
975
|
+
} = useMultiSelectState(value, onChange, onBlur);
|
|
976
|
+
const effectiveMaxDisplay = expanded ? Infinity : maxDisplay;
|
|
977
|
+
return /* @__PURE__ */ jsxs6(
|
|
978
|
+
FormFieldWrapper,
|
|
979
|
+
{
|
|
980
|
+
inputId: fieldId,
|
|
981
|
+
label: resolveFieldLabel(label),
|
|
982
|
+
description,
|
|
983
|
+
error,
|
|
984
|
+
required,
|
|
985
|
+
className: wrapperClassName,
|
|
986
|
+
children: [
|
|
987
|
+
/* @__PURE__ */ jsxs6(Popover2, { open, onOpenChange: handleOpenChange, children: [
|
|
988
|
+
/* @__PURE__ */ jsx7(PopoverTrigger2, { asChild: true, children: /* @__PURE__ */ jsx7(
|
|
989
|
+
MultiSelectTrigger,
|
|
990
|
+
{
|
|
991
|
+
id: fieldId,
|
|
992
|
+
open,
|
|
993
|
+
error,
|
|
994
|
+
className,
|
|
995
|
+
isDisabled,
|
|
996
|
+
autoFocus,
|
|
997
|
+
clearable,
|
|
998
|
+
hasValues: value.length > 0,
|
|
999
|
+
selectedOptions,
|
|
1000
|
+
effectiveMaxDisplay,
|
|
1001
|
+
expanded,
|
|
1002
|
+
maxDisplay,
|
|
1003
|
+
badgeColor,
|
|
1004
|
+
placeholder,
|
|
1005
|
+
onRemove: handleRemove,
|
|
1006
|
+
onExpand: handleExpand,
|
|
1007
|
+
onCollapse: handleCollapse,
|
|
1008
|
+
onClearAll: handleClearAll
|
|
1009
|
+
}
|
|
1010
|
+
) }),
|
|
1011
|
+
/* @__PURE__ */ jsx7(
|
|
1012
|
+
PopoverContent2,
|
|
1013
|
+
{
|
|
1014
|
+
className: "w-[--radix-popover-trigger-width] p-0",
|
|
1015
|
+
align: "start",
|
|
1016
|
+
children: /* @__PURE__ */ jsx7(
|
|
1017
|
+
MultiSelectDropdown,
|
|
1018
|
+
{
|
|
1019
|
+
searchPlaceholder,
|
|
1020
|
+
searchQuery,
|
|
1021
|
+
onSearchChange: setSearchQuery,
|
|
1022
|
+
loading,
|
|
1023
|
+
emptyMessage,
|
|
1024
|
+
options,
|
|
1025
|
+
value,
|
|
1026
|
+
onSelect: handleSelect
|
|
1027
|
+
}
|
|
1028
|
+
)
|
|
1029
|
+
}
|
|
1030
|
+
)
|
|
1031
|
+
] }),
|
|
1032
|
+
name && value.map((v) => /* @__PURE__ */ jsx7("input", { type: "hidden", name, value: v }, v))
|
|
1033
|
+
]
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/FormTagsInput.tsx
|
|
1039
|
+
import * as React7 from "react";
|
|
1040
|
+
import { X as X3 } from "lucide-react";
|
|
1041
|
+
import { cn as cn8 } from "@petrarca/sonnet-core";
|
|
1042
|
+
import { Input as Input3 } from "@petrarca/sonnet-ui";
|
|
1043
|
+
import { Badge as Badge2 } from "@petrarca/sonnet-ui";
|
|
1044
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1045
|
+
function TagBadge({ tag, index, isDisabled, onRemove }) {
|
|
1046
|
+
return /* @__PURE__ */ jsxs7(Badge2, { variant: "secondary", className: "gap-1", children: [
|
|
1047
|
+
tag,
|
|
1048
|
+
!isDisabled && /* @__PURE__ */ jsx8(
|
|
1049
|
+
"button",
|
|
1050
|
+
{
|
|
1051
|
+
type: "button",
|
|
1052
|
+
className: "ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
1053
|
+
onKeyDown: (e) => {
|
|
1054
|
+
if (e.key === "Enter") {
|
|
1055
|
+
e.preventDefault();
|
|
1056
|
+
onRemove(index);
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
onMouseDown: (e) => {
|
|
1060
|
+
e.preventDefault();
|
|
1061
|
+
e.stopPropagation();
|
|
1062
|
+
},
|
|
1063
|
+
onClick: (e) => {
|
|
1064
|
+
e.stopPropagation();
|
|
1065
|
+
onRemove(index);
|
|
1066
|
+
},
|
|
1067
|
+
children: /* @__PURE__ */ jsx8(X3, { className: "h-3 w-3 text-muted-foreground hover:text-foreground" })
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
] });
|
|
1071
|
+
}
|
|
1072
|
+
function useTagManagement(value, onChange, allowDuplicates, maxTags, splitKeys) {
|
|
1073
|
+
const [inputValue, setInputValue] = React7.useState("");
|
|
1074
|
+
const addTag = React7.useCallback(
|
|
1075
|
+
(tag) => {
|
|
1076
|
+
const trimmedTag = tag.trim();
|
|
1077
|
+
if (!trimmedTag) return;
|
|
1078
|
+
if (!allowDuplicates && value.includes(trimmedTag)) return;
|
|
1079
|
+
if (maxTags && value.length >= maxTags) return;
|
|
1080
|
+
onChange?.([...value, trimmedTag]);
|
|
1081
|
+
setInputValue("");
|
|
1082
|
+
},
|
|
1083
|
+
[value, onChange, allowDuplicates, maxTags]
|
|
1084
|
+
);
|
|
1085
|
+
const removeTag = React7.useCallback(
|
|
1086
|
+
(indexToRemove) => {
|
|
1087
|
+
onChange?.(value.filter((_, index) => index !== indexToRemove));
|
|
1088
|
+
},
|
|
1089
|
+
[value, onChange]
|
|
1090
|
+
);
|
|
1091
|
+
const handleKeyDown = React7.useCallback(
|
|
1092
|
+
(e) => {
|
|
1093
|
+
if (splitKeys.includes(e.key)) {
|
|
1094
|
+
e.preventDefault();
|
|
1095
|
+
addTag(inputValue);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (e.key === "Backspace" && !inputValue && value.length > 0) {
|
|
1099
|
+
e.preventDefault();
|
|
1100
|
+
removeTag(value.length - 1);
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
[splitKeys, inputValue, value.length, addTag, removeTag]
|
|
1104
|
+
);
|
|
1105
|
+
const handleInputChange = React7.useCallback(
|
|
1106
|
+
(e) => {
|
|
1107
|
+
const newValue = e.target.value;
|
|
1108
|
+
if (!splitKeys.includes(",") || !newValue.includes(",")) {
|
|
1109
|
+
setInputValue(newValue);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const parts = newValue.split(",");
|
|
1113
|
+
parts.slice(0, -1).forEach((tag) => addTag(tag));
|
|
1114
|
+
setInputValue(parts[parts.length - 1]);
|
|
1115
|
+
},
|
|
1116
|
+
[splitKeys, addTag]
|
|
1117
|
+
);
|
|
1118
|
+
return { inputValue, addTag, removeTag, handleKeyDown, handleInputChange };
|
|
1119
|
+
}
|
|
1120
|
+
function resolveTagsDefaults(props) {
|
|
1121
|
+
return {
|
|
1122
|
+
required: props.required ?? false,
|
|
1123
|
+
placeholder: props.placeholder ?? "Enter tag and press Enter",
|
|
1124
|
+
splitKeys: props.splitKeys ?? ["Enter", ","],
|
|
1125
|
+
allowDuplicates: props.allowDuplicates ?? false
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
var FormTagsInput = React7.forwardRef((props, ref) => {
|
|
1129
|
+
const {
|
|
1130
|
+
label,
|
|
1131
|
+
description,
|
|
1132
|
+
error,
|
|
1133
|
+
wrapperClassName,
|
|
1134
|
+
id,
|
|
1135
|
+
name,
|
|
1136
|
+
value = [],
|
|
1137
|
+
onChange,
|
|
1138
|
+
disabled,
|
|
1139
|
+
readOnly,
|
|
1140
|
+
maxTags
|
|
1141
|
+
} = props;
|
|
1142
|
+
const { required, placeholder, splitKeys, allowDuplicates } = resolveTagsDefaults(props);
|
|
1143
|
+
const generatedId = React7.useId();
|
|
1144
|
+
const fieldId = id ?? generatedId;
|
|
1145
|
+
const inputRef = React7.useRef(null);
|
|
1146
|
+
React7.useImperativeHandle(ref, () => inputRef.current);
|
|
1147
|
+
const isDisabled = disabled || readOnly;
|
|
1148
|
+
const { inputValue, removeTag, handleKeyDown, handleInputChange } = useTagManagement(value, onChange, allowDuplicates, maxTags, splitKeys);
|
|
1149
|
+
const handleContainerClick = () => {
|
|
1150
|
+
inputRef.current?.focus();
|
|
1151
|
+
};
|
|
1152
|
+
return /* @__PURE__ */ jsx8(
|
|
1153
|
+
FormFieldWrapper,
|
|
1154
|
+
{
|
|
1155
|
+
inputId: fieldId,
|
|
1156
|
+
label: resolveFieldLabel(label),
|
|
1157
|
+
description,
|
|
1158
|
+
error,
|
|
1159
|
+
required,
|
|
1160
|
+
className: wrapperClassName,
|
|
1161
|
+
children: /* @__PURE__ */ jsxs7(
|
|
1162
|
+
"div",
|
|
1163
|
+
{
|
|
1164
|
+
className: cn8(
|
|
1165
|
+
"flex min-h-10 w-full flex-wrap gap-1.5 rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
1166
|
+
isDisabled && "cursor-not-allowed opacity-50",
|
|
1167
|
+
error && "border-destructive"
|
|
1168
|
+
),
|
|
1169
|
+
onClick: handleContainerClick,
|
|
1170
|
+
children: [
|
|
1171
|
+
value.map((tag, index) => /* @__PURE__ */ jsx8(
|
|
1172
|
+
TagBadge,
|
|
1173
|
+
{
|
|
1174
|
+
tag,
|
|
1175
|
+
index,
|
|
1176
|
+
isDisabled: !!isDisabled,
|
|
1177
|
+
onRemove: removeTag
|
|
1178
|
+
},
|
|
1179
|
+
index
|
|
1180
|
+
)),
|
|
1181
|
+
/* @__PURE__ */ jsx8(
|
|
1182
|
+
Input3,
|
|
1183
|
+
{
|
|
1184
|
+
ref: inputRef,
|
|
1185
|
+
id: fieldId,
|
|
1186
|
+
name,
|
|
1187
|
+
value: inputValue,
|
|
1188
|
+
onChange: handleInputChange,
|
|
1189
|
+
onKeyDown: handleKeyDown,
|
|
1190
|
+
placeholder: value.length === 0 ? placeholder : "",
|
|
1191
|
+
disabled: !!isDisabled,
|
|
1192
|
+
className: "flex-1 min-w-[120px] border-0 p-0 h-auto focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
1193
|
+
}
|
|
1194
|
+
)
|
|
1195
|
+
]
|
|
1196
|
+
}
|
|
1197
|
+
)
|
|
1198
|
+
}
|
|
1199
|
+
);
|
|
1200
|
+
});
|
|
1201
|
+
FormTagsInput.displayName = "FormTagsInput";
|
|
1202
|
+
|
|
1203
|
+
// src/FormQuantityInput.tsx
|
|
1204
|
+
import React8, { useCallback as useCallback2, useEffect as useEffect2, useId as useId8, useMemo, useState as useState4 } from "react";
|
|
1205
|
+
import { ChevronsUpDown as ChevronsUpDown3 } from "lucide-react";
|
|
1206
|
+
import { cn as cn9 } from "@petrarca/sonnet-core";
|
|
1207
|
+
import {
|
|
1208
|
+
InputGroup,
|
|
1209
|
+
InputGroupAddon,
|
|
1210
|
+
InputGroupInput
|
|
1211
|
+
} from "@petrarca/sonnet-ui";
|
|
1212
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1213
|
+
var VALID_NUMERIC_INPUT = /^-?\d*[.,]?\d*$/;
|
|
1214
|
+
var INCOMPLETE_SENTINELS = /* @__PURE__ */ new Set(["", ".", "-", "-."]);
|
|
1215
|
+
function roundToPrecision(n, p) {
|
|
1216
|
+
const factor = Math.pow(10, p);
|
|
1217
|
+
return Math.round((n + Number.EPSILON) * factor) / factor;
|
|
1218
|
+
}
|
|
1219
|
+
function validateQuantity(numValue, lo, hi, unitLabel) {
|
|
1220
|
+
if (lo !== void 0 && numValue < lo) {
|
|
1221
|
+
return `Value must be at least ${lo}${unitLabel ? ` ${unitLabel}` : ""}`;
|
|
1222
|
+
}
|
|
1223
|
+
if (hi !== void 0 && numValue > hi) {
|
|
1224
|
+
return `Value must be at most ${hi}${unitLabel ? ` ${unitLabel}` : ""}`;
|
|
1225
|
+
}
|
|
1226
|
+
return "";
|
|
1227
|
+
}
|
|
1228
|
+
function roundAndFormat(numValue, precision) {
|
|
1229
|
+
const rounded = precision !== void 0 ? roundToPrecision(numValue, precision) : numValue;
|
|
1230
|
+
const formatted = precision !== void 0 ? rounded.toFixed(precision) : String(rounded);
|
|
1231
|
+
return { rounded, formatted };
|
|
1232
|
+
}
|
|
1233
|
+
function UnitAddon({
|
|
1234
|
+
units,
|
|
1235
|
+
currentUnit,
|
|
1236
|
+
addonAlign,
|
|
1237
|
+
disabled,
|
|
1238
|
+
readOnly,
|
|
1239
|
+
onChange
|
|
1240
|
+
}) {
|
|
1241
|
+
if (units.length === 0) return null;
|
|
1242
|
+
if (units.length === 1) {
|
|
1243
|
+
return /* @__PURE__ */ jsx9(InputGroupAddon, { align: addonAlign, children: /* @__PURE__ */ jsx9("span", { className: "text-sm text-muted-foreground", children: units[0].label }) });
|
|
1244
|
+
}
|
|
1245
|
+
return /* @__PURE__ */ jsx9(InputGroupAddon, { align: addonAlign, children: /* @__PURE__ */ jsxs8("div", { className: "relative flex items-center", children: [
|
|
1246
|
+
/* @__PURE__ */ jsx9(
|
|
1247
|
+
"select",
|
|
1248
|
+
{
|
|
1249
|
+
value: currentUnit,
|
|
1250
|
+
onChange,
|
|
1251
|
+
disabled: disabled || readOnly,
|
|
1252
|
+
className: "cursor-pointer appearance-none border-0 bg-transparent py-0 pr-5 pl-1 text-sm text-muted-foreground outline-none hover:text-foreground focus:text-foreground",
|
|
1253
|
+
"aria-label": "Unit",
|
|
1254
|
+
children: units.map((u) => /* @__PURE__ */ jsx9("option", { value: u.value, children: u.label }, u.value))
|
|
1255
|
+
}
|
|
1256
|
+
),
|
|
1257
|
+
/* @__PURE__ */ jsx9(ChevronsUpDown3, { className: "pointer-events-none absolute right-0 h-3.5 w-3.5 text-muted-foreground/60" })
|
|
1258
|
+
] }) });
|
|
1259
|
+
}
|
|
1260
|
+
function useUnitState(value, units, unitPosition, min, max) {
|
|
1261
|
+
const currentUnit = useMemo(() => {
|
|
1262
|
+
const preferred = value?.unit;
|
|
1263
|
+
if (preferred && units.some((u) => u.value === preferred)) return preferred;
|
|
1264
|
+
return units[0]?.value ?? "";
|
|
1265
|
+
}, [value?.unit, units]);
|
|
1266
|
+
const unitMeta = useMemo(
|
|
1267
|
+
() => units.find((u) => u.value === currentUnit),
|
|
1268
|
+
[units, currentUnit]
|
|
1269
|
+
);
|
|
1270
|
+
const effectiveMin = unitMeta?.min ?? min;
|
|
1271
|
+
const effectiveMax = unitMeta?.max ?? max;
|
|
1272
|
+
const effectivePosition = unitMeta?.position ?? unitPosition;
|
|
1273
|
+
const addonAlign = effectivePosition === "prefix" ? "inline-start" : "inline-end";
|
|
1274
|
+
return { currentUnit, effectiveMin, effectiveMax, addonAlign };
|
|
1275
|
+
}
|
|
1276
|
+
function useQuantityInput(value, onChange, onBlur, currentUnit, precision, effectiveMin, effectiveMax, externalError) {
|
|
1277
|
+
const [rawInput, setRawInput] = useState4(
|
|
1278
|
+
value?.value !== void 0 ? String(value.value) : ""
|
|
1279
|
+
);
|
|
1280
|
+
const [validationError, setValidationError] = useState4("");
|
|
1281
|
+
useEffect2(() => {
|
|
1282
|
+
const external = value?.value !== void 0 ? String(value.value) : "";
|
|
1283
|
+
setRawInput((prev) => {
|
|
1284
|
+
const parsed = parseFloat(prev.replace(",", "."));
|
|
1285
|
+
if (!isNaN(parsed) && parsed === value?.value) return prev;
|
|
1286
|
+
return external;
|
|
1287
|
+
});
|
|
1288
|
+
}, [value?.value]);
|
|
1289
|
+
const error = externalError || validationError;
|
|
1290
|
+
const emit = useCallback2(
|
|
1291
|
+
(numValue, unitValue) => {
|
|
1292
|
+
onChange?.({ value: numValue, unit: unitValue });
|
|
1293
|
+
},
|
|
1294
|
+
[onChange]
|
|
1295
|
+
);
|
|
1296
|
+
const handleInputChange = useCallback2(
|
|
1297
|
+
(e) => {
|
|
1298
|
+
const raw = e.target.value;
|
|
1299
|
+
if (raw !== "" && !VALID_NUMERIC_INPUT.test(raw)) return;
|
|
1300
|
+
setRawInput(raw);
|
|
1301
|
+
setValidationError("");
|
|
1302
|
+
},
|
|
1303
|
+
[]
|
|
1304
|
+
);
|
|
1305
|
+
const handleInputBlur = useCallback2(() => {
|
|
1306
|
+
const normalized = rawInput.replace(",", ".");
|
|
1307
|
+
if (INCOMPLETE_SENTINELS.has(normalized)) {
|
|
1308
|
+
if (normalized !== "") setRawInput("");
|
|
1309
|
+
emit(void 0, currentUnit);
|
|
1310
|
+
setValidationError("");
|
|
1311
|
+
onBlur?.();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const numValue = parseFloat(normalized);
|
|
1315
|
+
if (isNaN(numValue)) {
|
|
1316
|
+
setRawInput("");
|
|
1317
|
+
emit(void 0, currentUnit);
|
|
1318
|
+
onBlur?.();
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const { rounded, formatted } = roundAndFormat(numValue, precision);
|
|
1322
|
+
setRawInput(formatted);
|
|
1323
|
+
setValidationError(
|
|
1324
|
+
validateQuantity(rounded, effectiveMin, effectiveMax, currentUnit)
|
|
1325
|
+
);
|
|
1326
|
+
emit(rounded, currentUnit);
|
|
1327
|
+
onBlur?.();
|
|
1328
|
+
}, [
|
|
1329
|
+
rawInput,
|
|
1330
|
+
currentUnit,
|
|
1331
|
+
precision,
|
|
1332
|
+
effectiveMin,
|
|
1333
|
+
effectiveMax,
|
|
1334
|
+
emit,
|
|
1335
|
+
onBlur
|
|
1336
|
+
]);
|
|
1337
|
+
return {
|
|
1338
|
+
rawInput,
|
|
1339
|
+
error,
|
|
1340
|
+
handleInputChange,
|
|
1341
|
+
handleInputBlur,
|
|
1342
|
+
emit,
|
|
1343
|
+
setValidationError
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
var FormQuantityInput = React8.forwardRef((allProps, ref) => {
|
|
1347
|
+
const {
|
|
1348
|
+
value,
|
|
1349
|
+
onChange,
|
|
1350
|
+
onBlur,
|
|
1351
|
+
precision,
|
|
1352
|
+
step,
|
|
1353
|
+
min,
|
|
1354
|
+
max,
|
|
1355
|
+
label,
|
|
1356
|
+
description,
|
|
1357
|
+
placeholder,
|
|
1358
|
+
required,
|
|
1359
|
+
disabled,
|
|
1360
|
+
readOnly,
|
|
1361
|
+
autoFocus,
|
|
1362
|
+
wrapperClassName,
|
|
1363
|
+
className,
|
|
1364
|
+
name
|
|
1365
|
+
} = allProps;
|
|
1366
|
+
const units = useMemo(() => allProps.units ?? [], [allProps.units]);
|
|
1367
|
+
const unitPosition = allProps.unitPosition ?? "suffix";
|
|
1368
|
+
const error = allProps.error;
|
|
1369
|
+
const generatedId = useId8();
|
|
1370
|
+
const inputId = allProps.id ?? generatedId;
|
|
1371
|
+
const { currentUnit, effectiveMin, effectiveMax, addonAlign } = useUnitState(
|
|
1372
|
+
value,
|
|
1373
|
+
units,
|
|
1374
|
+
unitPosition,
|
|
1375
|
+
min,
|
|
1376
|
+
max
|
|
1377
|
+
);
|
|
1378
|
+
const {
|
|
1379
|
+
rawInput,
|
|
1380
|
+
error: resolvedError,
|
|
1381
|
+
handleInputChange,
|
|
1382
|
+
handleInputBlur,
|
|
1383
|
+
emit,
|
|
1384
|
+
setValidationError
|
|
1385
|
+
} = useQuantityInput(
|
|
1386
|
+
value,
|
|
1387
|
+
onChange,
|
|
1388
|
+
onBlur,
|
|
1389
|
+
currentUnit,
|
|
1390
|
+
precision,
|
|
1391
|
+
effectiveMin,
|
|
1392
|
+
effectiveMax,
|
|
1393
|
+
error
|
|
1394
|
+
);
|
|
1395
|
+
const handleUnitChange = useCallback2(
|
|
1396
|
+
(e) => {
|
|
1397
|
+
const unitValue = e.target.value;
|
|
1398
|
+
setValidationError("");
|
|
1399
|
+
const normalized = rawInput.replace(",", ".");
|
|
1400
|
+
const numValue = parseFloat(normalized);
|
|
1401
|
+
if (isNaN(numValue)) return;
|
|
1402
|
+
const um = units.find((u) => u.value === unitValue);
|
|
1403
|
+
const lo = um?.min ?? min;
|
|
1404
|
+
const hi = um?.max ?? max;
|
|
1405
|
+
setValidationError(validateQuantity(numValue, lo, hi, unitValue));
|
|
1406
|
+
emit(numValue, unitValue);
|
|
1407
|
+
},
|
|
1408
|
+
[rawInput, units, min, max, emit, setValidationError]
|
|
1409
|
+
);
|
|
1410
|
+
const unitAddon = useMemo(
|
|
1411
|
+
() => /* @__PURE__ */ jsx9(
|
|
1412
|
+
UnitAddon,
|
|
1413
|
+
{
|
|
1414
|
+
units,
|
|
1415
|
+
currentUnit,
|
|
1416
|
+
addonAlign,
|
|
1417
|
+
disabled,
|
|
1418
|
+
readOnly,
|
|
1419
|
+
onChange: handleUnitChange
|
|
1420
|
+
}
|
|
1421
|
+
),
|
|
1422
|
+
[units, currentUnit, addonAlign, disabled, readOnly, handleUnitChange]
|
|
1423
|
+
);
|
|
1424
|
+
return /* @__PURE__ */ jsx9(
|
|
1425
|
+
FormFieldWrapper,
|
|
1426
|
+
{
|
|
1427
|
+
inputId,
|
|
1428
|
+
label,
|
|
1429
|
+
description,
|
|
1430
|
+
error: resolvedError,
|
|
1431
|
+
required,
|
|
1432
|
+
className: wrapperClassName,
|
|
1433
|
+
children: /* @__PURE__ */ jsxs8(
|
|
1434
|
+
InputGroup,
|
|
1435
|
+
{
|
|
1436
|
+
"data-disabled": disabled || void 0,
|
|
1437
|
+
className: cn9(resolvedError && "border-destructive"),
|
|
1438
|
+
children: [
|
|
1439
|
+
/* @__PURE__ */ jsx9(
|
|
1440
|
+
InputGroupInput,
|
|
1441
|
+
{
|
|
1442
|
+
id: inputId,
|
|
1443
|
+
name,
|
|
1444
|
+
ref,
|
|
1445
|
+
type: "text",
|
|
1446
|
+
inputMode: "decimal",
|
|
1447
|
+
value: rawInput,
|
|
1448
|
+
onChange: handleInputChange,
|
|
1449
|
+
onBlur: handleInputBlur,
|
|
1450
|
+
placeholder,
|
|
1451
|
+
disabled,
|
|
1452
|
+
readOnly,
|
|
1453
|
+
autoFocus,
|
|
1454
|
+
step,
|
|
1455
|
+
className,
|
|
1456
|
+
"aria-invalid": resolvedError ? "true" : "false",
|
|
1457
|
+
"aria-describedby": getAriaDescribedBy(
|
|
1458
|
+
inputId,
|
|
1459
|
+
resolvedError,
|
|
1460
|
+
description
|
|
1461
|
+
),
|
|
1462
|
+
"aria-required": required,
|
|
1463
|
+
"aria-valuemin": effectiveMin,
|
|
1464
|
+
"aria-valuemax": effectiveMax
|
|
1465
|
+
}
|
|
1466
|
+
),
|
|
1467
|
+
unitAddon
|
|
1468
|
+
]
|
|
1469
|
+
}
|
|
1470
|
+
)
|
|
1471
|
+
}
|
|
1472
|
+
);
|
|
1473
|
+
});
|
|
1474
|
+
FormQuantityInput.displayName = "FormQuantityInput";
|
|
1475
|
+
|
|
1476
|
+
// src/FormActions.tsx
|
|
1477
|
+
import { Button as Button3 } from "@petrarca/sonnet-ui";
|
|
1478
|
+
import { SimpleGroup } from "@petrarca/sonnet-ui";
|
|
1479
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1480
|
+
function FormActions({
|
|
1481
|
+
onSave,
|
|
1482
|
+
onCancel,
|
|
1483
|
+
disabled = false,
|
|
1484
|
+
hasChanges,
|
|
1485
|
+
isValid,
|
|
1486
|
+
saveLabel = "Save",
|
|
1487
|
+
cancelLabel = "Cancel",
|
|
1488
|
+
showCancel = true
|
|
1489
|
+
}) {
|
|
1490
|
+
return /* @__PURE__ */ jsxs9(SimpleGroup, { justify: "flex-end", mt: "md", children: [
|
|
1491
|
+
showCancel && /* @__PURE__ */ jsx10(Button3, { variant: "outline", onClick: onCancel, disabled, children: cancelLabel }),
|
|
1492
|
+
/* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: disabled || !hasChanges || !isValid, children: saveLabel })
|
|
1493
|
+
] });
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/ExtraPropertiesCard.tsx
|
|
1497
|
+
import { useMemo as useMemo2 } from "react";
|
|
1498
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
1499
|
+
import { Card } from "@petrarca/sonnet-ui";
|
|
1500
|
+
import { useDisclosure } from "@petrarca/sonnet-core/hooks";
|
|
1501
|
+
import { Collapsible, CollapsibleContent } from "@petrarca/sonnet-ui";
|
|
1502
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1503
|
+
function ExtraPropertiesCard({ properties, schema }) {
|
|
1504
|
+
const [opened, { toggle }] = useDisclosure(false);
|
|
1505
|
+
const extraProperties = useMemo2(() => {
|
|
1506
|
+
if (!schema) return null;
|
|
1507
|
+
const schemaProps = schema.properties ?? {};
|
|
1508
|
+
const extra = Object.fromEntries(
|
|
1509
|
+
Object.entries(properties).filter(([k]) => !(k in schemaProps))
|
|
1510
|
+
);
|
|
1511
|
+
return Object.keys(extra).length > 0 ? extra : null;
|
|
1512
|
+
}, [properties, schema]);
|
|
1513
|
+
if (!extraProperties) return null;
|
|
1514
|
+
return /* @__PURE__ */ jsxs10(Card, { className: "border p-3", children: [
|
|
1515
|
+
/* @__PURE__ */ jsxs10(
|
|
1516
|
+
"p",
|
|
1517
|
+
{
|
|
1518
|
+
className: `flex items-center gap-1 text-sm font-medium cursor-pointer select-none ${opened ? "mb-1" : ""}`,
|
|
1519
|
+
onClick: toggle,
|
|
1520
|
+
children: [
|
|
1521
|
+
opened ? /* @__PURE__ */ jsx11(ChevronDown, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx11(ChevronRight, { className: "h-3.5 w-3.5" }),
|
|
1522
|
+
"Extra Properties (not in schema)"
|
|
1523
|
+
]
|
|
1524
|
+
}
|
|
1525
|
+
),
|
|
1526
|
+
/* @__PURE__ */ jsx11(Collapsible, { open: opened, children: /* @__PURE__ */ jsx11(CollapsibleContent, { children: /* @__PURE__ */ jsx11("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx11("code", { children: JSON.stringify(extraProperties, null, 2) }) }) }) })
|
|
1527
|
+
] });
|
|
1528
|
+
}
|
|
1529
|
+
var ExtraPropertiesCard_default = ExtraPropertiesCard;
|
|
1530
|
+
|
|
1531
|
+
// src/JsonSchemaFormRenderer.tsx
|
|
1532
|
+
import React11, { useCallback as useCallback8, useMemo as useMemo7 } from "react";
|
|
1533
|
+
import { SimpleStack } from "@petrarca/sonnet-ui";
|
|
1534
|
+
import { isFieldRequired } from "@petrarca/sonnet-core/schema";
|
|
1535
|
+
import { FORM_ITEM_ID_FIELD as FORM_ITEM_ID_FIELD2 } from "@petrarca/sonnet-core";
|
|
1536
|
+
|
|
1537
|
+
// src/widgets/types.ts
|
|
1538
|
+
function firstError(rawErrors) {
|
|
1539
|
+
return rawErrors && rawErrors.length > 0 ? rawErrors[0] : void 0;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/widgets/TextInputWidget.tsx
|
|
1543
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1544
|
+
function inputTypeFromFormat(format) {
|
|
1545
|
+
if (format === "email") return "email";
|
|
1546
|
+
if (format === "uri" || format === "url") return "url";
|
|
1547
|
+
return "text";
|
|
1548
|
+
}
|
|
1549
|
+
function TextInputWidget({
|
|
1550
|
+
id,
|
|
1551
|
+
name,
|
|
1552
|
+
value,
|
|
1553
|
+
onChange,
|
|
1554
|
+
onBlur,
|
|
1555
|
+
propertySchema,
|
|
1556
|
+
required,
|
|
1557
|
+
disabled,
|
|
1558
|
+
readonly,
|
|
1559
|
+
rawErrors,
|
|
1560
|
+
label,
|
|
1561
|
+
description,
|
|
1562
|
+
placeholder,
|
|
1563
|
+
autofocus,
|
|
1564
|
+
options
|
|
1565
|
+
}) {
|
|
1566
|
+
const error = firstError(rawErrors);
|
|
1567
|
+
const compact = options?.compact;
|
|
1568
|
+
return /* @__PURE__ */ jsx12(
|
|
1569
|
+
FormInput,
|
|
1570
|
+
{
|
|
1571
|
+
id,
|
|
1572
|
+
name,
|
|
1573
|
+
value: value !== void 0 && value !== null ? String(value) : "",
|
|
1574
|
+
onChange: (event) => onChange(event.currentTarget.value),
|
|
1575
|
+
onBlur: () => onBlur?.(id, value),
|
|
1576
|
+
label,
|
|
1577
|
+
description,
|
|
1578
|
+
placeholder,
|
|
1579
|
+
required,
|
|
1580
|
+
disabled,
|
|
1581
|
+
readOnly: readonly,
|
|
1582
|
+
error,
|
|
1583
|
+
type: options?.inputType ?? inputTypeFromFormat(propertySchema.format),
|
|
1584
|
+
minLength: propertySchema.minLength,
|
|
1585
|
+
maxLength: propertySchema.maxLength,
|
|
1586
|
+
autoFocus: autofocus,
|
|
1587
|
+
compact,
|
|
1588
|
+
className: compact ? "h-8 text-xs" : void 0
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/widgets/TextareaWidget.tsx
|
|
1594
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
1595
|
+
function TextareaWidget({
|
|
1596
|
+
id,
|
|
1597
|
+
name,
|
|
1598
|
+
value,
|
|
1599
|
+
onChange,
|
|
1600
|
+
onBlur,
|
|
1601
|
+
propertySchema,
|
|
1602
|
+
required,
|
|
1603
|
+
disabled,
|
|
1604
|
+
readonly,
|
|
1605
|
+
rawErrors,
|
|
1606
|
+
label,
|
|
1607
|
+
description,
|
|
1608
|
+
placeholder,
|
|
1609
|
+
autofocus,
|
|
1610
|
+
options = {}
|
|
1611
|
+
}) {
|
|
1612
|
+
const minRows = options.rows || options.minRows || 3;
|
|
1613
|
+
const autosize = options.autosize !== false;
|
|
1614
|
+
const error = firstError(rawErrors);
|
|
1615
|
+
return /* @__PURE__ */ jsx13(
|
|
1616
|
+
FormTextarea,
|
|
1617
|
+
{
|
|
1618
|
+
id,
|
|
1619
|
+
name,
|
|
1620
|
+
value: value !== void 0 && value !== null ? String(value) : "",
|
|
1621
|
+
onChange: (event) => onChange(event.currentTarget.value),
|
|
1622
|
+
onBlur: () => onBlur?.(id, value),
|
|
1623
|
+
label,
|
|
1624
|
+
description,
|
|
1625
|
+
placeholder,
|
|
1626
|
+
required,
|
|
1627
|
+
disabled,
|
|
1628
|
+
readOnly: readonly,
|
|
1629
|
+
error,
|
|
1630
|
+
minRows,
|
|
1631
|
+
autosize,
|
|
1632
|
+
minLength: propertySchema.minLength,
|
|
1633
|
+
maxLength: propertySchema.maxLength,
|
|
1634
|
+
autoFocus: autofocus
|
|
1635
|
+
}
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/widgets/NumberWidget.tsx
|
|
1640
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
1641
|
+
function NumberWidget({
|
|
1642
|
+
id,
|
|
1643
|
+
name,
|
|
1644
|
+
value,
|
|
1645
|
+
onChange,
|
|
1646
|
+
onBlur,
|
|
1647
|
+
propertySchema,
|
|
1648
|
+
required,
|
|
1649
|
+
disabled,
|
|
1650
|
+
readonly,
|
|
1651
|
+
rawErrors,
|
|
1652
|
+
label,
|
|
1653
|
+
description,
|
|
1654
|
+
placeholder,
|
|
1655
|
+
autofocus,
|
|
1656
|
+
options = {}
|
|
1657
|
+
}) {
|
|
1658
|
+
const isInteger = propertySchema.type === "integer";
|
|
1659
|
+
const step = options.step ?? (isInteger ? 1 : void 0);
|
|
1660
|
+
const allowDecimal = options.allowDecimal ?? !isInteger;
|
|
1661
|
+
const error = firstError(rawErrors);
|
|
1662
|
+
return /* @__PURE__ */ jsx14(
|
|
1663
|
+
FormNumberInput,
|
|
1664
|
+
{
|
|
1665
|
+
id,
|
|
1666
|
+
name,
|
|
1667
|
+
value: value !== void 0 && value !== null ? Number(value) : void 0,
|
|
1668
|
+
onChange: (val) => onChange(val),
|
|
1669
|
+
onBlur: () => onBlur?.(id, value),
|
|
1670
|
+
label,
|
|
1671
|
+
description,
|
|
1672
|
+
placeholder,
|
|
1673
|
+
required,
|
|
1674
|
+
disabled,
|
|
1675
|
+
readOnly: readonly,
|
|
1676
|
+
error,
|
|
1677
|
+
min: propertySchema.minimum,
|
|
1678
|
+
max: propertySchema.maximum,
|
|
1679
|
+
step,
|
|
1680
|
+
allowDecimal,
|
|
1681
|
+
autoFocus: autofocus
|
|
1682
|
+
}
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// src/widgets/CheckboxWidget.tsx
|
|
1687
|
+
import { useCallback as useCallback3 } from "react";
|
|
1688
|
+
import { isNullableBoolean } from "@petrarca/sonnet-core/schema";
|
|
1689
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
1690
|
+
function CheckboxWidget({
|
|
1691
|
+
id,
|
|
1692
|
+
value,
|
|
1693
|
+
onChange,
|
|
1694
|
+
onBlur,
|
|
1695
|
+
disabled,
|
|
1696
|
+
readonly,
|
|
1697
|
+
rawErrors,
|
|
1698
|
+
label,
|
|
1699
|
+
description,
|
|
1700
|
+
autofocus,
|
|
1701
|
+
propertySchema,
|
|
1702
|
+
options
|
|
1703
|
+
}) {
|
|
1704
|
+
const error = firstError(rawErrors);
|
|
1705
|
+
const compact = options?.compact;
|
|
1706
|
+
const isTriState = isNullableBoolean(propertySchema);
|
|
1707
|
+
const isChecked = value === true;
|
|
1708
|
+
const isIndeterminate = isTriState && value === null;
|
|
1709
|
+
const checkedState = isIndeterminate ? "indeterminate" : isChecked;
|
|
1710
|
+
const handleChange = useCallback3(
|
|
1711
|
+
(_newChecked) => {
|
|
1712
|
+
if (isTriState) {
|
|
1713
|
+
if (value === null) {
|
|
1714
|
+
onChange(true);
|
|
1715
|
+
} else if (value === true) {
|
|
1716
|
+
onChange(false);
|
|
1717
|
+
} else {
|
|
1718
|
+
onChange(null);
|
|
1719
|
+
}
|
|
1720
|
+
} else {
|
|
1721
|
+
onChange(_newChecked === true);
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
1724
|
+
[isTriState, value, onChange]
|
|
1725
|
+
);
|
|
1726
|
+
return /* @__PURE__ */ jsx15(
|
|
1727
|
+
FormCheckbox,
|
|
1728
|
+
{
|
|
1729
|
+
id,
|
|
1730
|
+
checked: checkedState,
|
|
1731
|
+
onChange: handleChange,
|
|
1732
|
+
onBlur: () => onBlur?.(id, value),
|
|
1733
|
+
label,
|
|
1734
|
+
description,
|
|
1735
|
+
disabled,
|
|
1736
|
+
readOnly: readonly,
|
|
1737
|
+
error,
|
|
1738
|
+
autoFocus: autofocus,
|
|
1739
|
+
compact
|
|
1740
|
+
}
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// src/widgets/SelectWidget.tsx
|
|
1745
|
+
import { useRef as useRef3 } from "react";
|
|
1746
|
+
import { extractEnumValues } from "@petrarca/sonnet-core/schema";
|
|
1747
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
1748
|
+
function buildSelectOptions(propertySchema, enumNames) {
|
|
1749
|
+
const enumValues = extractEnumValues(propertySchema);
|
|
1750
|
+
return enumValues.map((val) => {
|
|
1751
|
+
const key = String(val);
|
|
1752
|
+
return { value: key, label: enumNames[key] || key };
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
function normalizeMultiValue(value) {
|
|
1756
|
+
if (Array.isArray(value)) return value.map(String);
|
|
1757
|
+
if (value) return [String(value)];
|
|
1758
|
+
return [];
|
|
1759
|
+
}
|
|
1760
|
+
function normalizeSingleValue(value) {
|
|
1761
|
+
if (value !== void 0 && value !== null) return String(value);
|
|
1762
|
+
return null;
|
|
1763
|
+
}
|
|
1764
|
+
function SelectWidget(props) {
|
|
1765
|
+
const {
|
|
1766
|
+
id,
|
|
1767
|
+
name,
|
|
1768
|
+
value,
|
|
1769
|
+
onChange,
|
|
1770
|
+
onBlur,
|
|
1771
|
+
propertySchema,
|
|
1772
|
+
required,
|
|
1773
|
+
disabled,
|
|
1774
|
+
readonly,
|
|
1775
|
+
rawErrors,
|
|
1776
|
+
label,
|
|
1777
|
+
description,
|
|
1778
|
+
placeholder,
|
|
1779
|
+
autofocus
|
|
1780
|
+
} = props;
|
|
1781
|
+
const widgetOptions = props.options ?? {};
|
|
1782
|
+
const valueRef = useRef3(value);
|
|
1783
|
+
valueRef.current = value;
|
|
1784
|
+
const enumNames = widgetOptions.enumNames || {};
|
|
1785
|
+
const clearable = widgetOptions.clearable !== false && !required;
|
|
1786
|
+
const isMultiple = widgetOptions.multiple ?? propertySchema.type === "array";
|
|
1787
|
+
const selectOptions = buildSelectOptions(propertySchema, enumNames);
|
|
1788
|
+
const error = firstError(rawErrors);
|
|
1789
|
+
const compact = widgetOptions?.compact;
|
|
1790
|
+
const triggerClassName = compact ? "h-8 text-xs" : void 0;
|
|
1791
|
+
const handleBlur = () => onBlur?.(id, valueRef.current);
|
|
1792
|
+
if (isMultiple) {
|
|
1793
|
+
return /* @__PURE__ */ jsx16(
|
|
1794
|
+
FormMultiSelect,
|
|
1795
|
+
{
|
|
1796
|
+
id,
|
|
1797
|
+
name,
|
|
1798
|
+
options: selectOptions,
|
|
1799
|
+
value: normalizeMultiValue(value),
|
|
1800
|
+
onChange: (newValue) => onChange(newValue || []),
|
|
1801
|
+
label,
|
|
1802
|
+
description,
|
|
1803
|
+
placeholder,
|
|
1804
|
+
required,
|
|
1805
|
+
disabled,
|
|
1806
|
+
readOnly: readonly,
|
|
1807
|
+
error,
|
|
1808
|
+
clearable,
|
|
1809
|
+
autoFocus: autofocus,
|
|
1810
|
+
badgeColor: widgetOptions.badgeColor,
|
|
1811
|
+
onBlur: handleBlur,
|
|
1812
|
+
className: triggerClassName
|
|
1813
|
+
}
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
return /* @__PURE__ */ jsx16(
|
|
1817
|
+
FormSelect,
|
|
1818
|
+
{
|
|
1819
|
+
id,
|
|
1820
|
+
name,
|
|
1821
|
+
options: selectOptions,
|
|
1822
|
+
value: normalizeSingleValue(value),
|
|
1823
|
+
onChange: (val) => onChange(val || ""),
|
|
1824
|
+
label,
|
|
1825
|
+
description,
|
|
1826
|
+
placeholder,
|
|
1827
|
+
required,
|
|
1828
|
+
disabled,
|
|
1829
|
+
readOnly: readonly,
|
|
1830
|
+
error,
|
|
1831
|
+
clearable,
|
|
1832
|
+
autoFocus: autofocus,
|
|
1833
|
+
onBlur: handleBlur,
|
|
1834
|
+
className: triggerClassName,
|
|
1835
|
+
compact
|
|
1836
|
+
}
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// src/widgets/TagsInputWidget.tsx
|
|
1841
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1842
|
+
function TagsInputWidget({
|
|
1843
|
+
id,
|
|
1844
|
+
name,
|
|
1845
|
+
value,
|
|
1846
|
+
onChange,
|
|
1847
|
+
required,
|
|
1848
|
+
disabled,
|
|
1849
|
+
readonly,
|
|
1850
|
+
rawErrors,
|
|
1851
|
+
label,
|
|
1852
|
+
description,
|
|
1853
|
+
placeholder,
|
|
1854
|
+
options
|
|
1855
|
+
}) {
|
|
1856
|
+
const error = firstError(rawErrors);
|
|
1857
|
+
const tags = Array.isArray(value) ? value.map(String) : [];
|
|
1858
|
+
return /* @__PURE__ */ jsx17(
|
|
1859
|
+
FormTagsInput,
|
|
1860
|
+
{
|
|
1861
|
+
id,
|
|
1862
|
+
name,
|
|
1863
|
+
value: tags,
|
|
1864
|
+
onChange: (newTags) => onChange(newTags),
|
|
1865
|
+
label,
|
|
1866
|
+
description,
|
|
1867
|
+
placeholder: placeholder ?? "Type and press Enter",
|
|
1868
|
+
required,
|
|
1869
|
+
disabled,
|
|
1870
|
+
readOnly: readonly,
|
|
1871
|
+
error,
|
|
1872
|
+
maxTags: options?.maxTags,
|
|
1873
|
+
splitKeys: options?.splitKeys,
|
|
1874
|
+
allowDuplicates: false
|
|
1875
|
+
}
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/widgets/JsonEditorWidget.tsx
|
|
1880
|
+
import { useCallback as useCallback4, useState as useState5 } from "react";
|
|
1881
|
+
import { JsonEditor } from "@petrarca/sonnet-ui/json-editor";
|
|
1882
|
+
import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1883
|
+
var FULL_DOCUMENT = ["/"];
|
|
1884
|
+
function JsonEditorWidget({
|
|
1885
|
+
id,
|
|
1886
|
+
value,
|
|
1887
|
+
onChange,
|
|
1888
|
+
label,
|
|
1889
|
+
description,
|
|
1890
|
+
required,
|
|
1891
|
+
options
|
|
1892
|
+
}) {
|
|
1893
|
+
const [raw, setRaw] = useState5(
|
|
1894
|
+
value != null ? JSON.stringify(value, null, 2) : ""
|
|
1895
|
+
);
|
|
1896
|
+
const height = options?.height ?? "240px";
|
|
1897
|
+
const handleChange = useCallback4(
|
|
1898
|
+
(val) => {
|
|
1899
|
+
setRaw(val);
|
|
1900
|
+
try {
|
|
1901
|
+
const parsed = JSON.parse(val);
|
|
1902
|
+
onChange(parsed);
|
|
1903
|
+
} catch {
|
|
1904
|
+
}
|
|
1905
|
+
},
|
|
1906
|
+
[onChange]
|
|
1907
|
+
);
|
|
1908
|
+
return /* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
|
|
1909
|
+
label && /* @__PURE__ */ jsxs11("label", { htmlFor: id, className: "text-sm font-medium leading-none", children: [
|
|
1910
|
+
label,
|
|
1911
|
+
required && /* @__PURE__ */ jsx18("span", { className: "text-destructive ml-1", children: "*" })
|
|
1912
|
+
] }),
|
|
1913
|
+
description && /* @__PURE__ */ jsx18("p", { className: "text-sm text-muted-foreground", children: description }),
|
|
1914
|
+
/* @__PURE__ */ jsx18(
|
|
1915
|
+
JsonEditor,
|
|
1916
|
+
{
|
|
1917
|
+
value: raw,
|
|
1918
|
+
editablePaths: FULL_DOCUMENT,
|
|
1919
|
+
onChange: handleChange,
|
|
1920
|
+
height,
|
|
1921
|
+
className: "rounded-md border overflow-hidden"
|
|
1922
|
+
}
|
|
1923
|
+
)
|
|
1924
|
+
] });
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/widgets/EntitySelectWidget.tsx
|
|
1928
|
+
import { useMemo as useMemo3 } from "react";
|
|
1929
|
+
import { EntitySelect } from "@petrarca/sonnet-ui";
|
|
1930
|
+
import { useFetcherFactory } from "@petrarca/sonnet-core/entityOptions";
|
|
1931
|
+
import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1932
|
+
var VALID_MODES = ["eager", "typeahead", "explicit"];
|
|
1933
|
+
function resolveSelectMode(options) {
|
|
1934
|
+
const rawMode = options?.mode;
|
|
1935
|
+
if (typeof rawMode === "string" && VALID_MODES.includes(rawMode)) {
|
|
1936
|
+
return rawMode;
|
|
1937
|
+
}
|
|
1938
|
+
return "eager";
|
|
1939
|
+
}
|
|
1940
|
+
function resolveMinChars(options) {
|
|
1941
|
+
const rawMinChars = options?.minChars;
|
|
1942
|
+
if (typeof rawMinChars === "number" && rawMinChars >= 1) return rawMinChars;
|
|
1943
|
+
return 1;
|
|
1944
|
+
}
|
|
1945
|
+
function buildRestConfig(options) {
|
|
1946
|
+
if (!("endpoint" in options) || !options.endpoint) return null;
|
|
1947
|
+
return {
|
|
1948
|
+
endpoint: String(options.endpoint),
|
|
1949
|
+
valueKey: String(options.valueKey ?? "id"),
|
|
1950
|
+
labelKey: String(options.labelKey ?? "name"),
|
|
1951
|
+
searchKey: options.searchKey ? String(options.searchKey) : void 0
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function buildGraphqlConfig(options) {
|
|
1955
|
+
if (!("query" in options) || !options.query) return null;
|
|
1956
|
+
return {
|
|
1957
|
+
query: String(options.query),
|
|
1958
|
+
dataPath: String(options.dataPath ?? ""),
|
|
1959
|
+
valueKey: String(options.valueKey ?? "id"),
|
|
1960
|
+
labelKey: String(options.labelKey ?? "name")
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
function buildFetcherConfig(options) {
|
|
1964
|
+
if (!options) return null;
|
|
1965
|
+
return buildRestConfig(options) ?? buildGraphqlConfig(options);
|
|
1966
|
+
}
|
|
1967
|
+
function deriveQueryKey(config, fallbackName) {
|
|
1968
|
+
if (!config) return fallbackName;
|
|
1969
|
+
if ("endpoint" in config) return config.endpoint;
|
|
1970
|
+
return `graphql:${config.dataPath}`;
|
|
1971
|
+
}
|
|
1972
|
+
function EntitySelectWidget({
|
|
1973
|
+
id,
|
|
1974
|
+
name,
|
|
1975
|
+
value,
|
|
1976
|
+
onChange,
|
|
1977
|
+
onBlur,
|
|
1978
|
+
label,
|
|
1979
|
+
description,
|
|
1980
|
+
placeholder,
|
|
1981
|
+
required,
|
|
1982
|
+
disabled,
|
|
1983
|
+
readonly,
|
|
1984
|
+
rawErrors,
|
|
1985
|
+
options
|
|
1986
|
+
}) {
|
|
1987
|
+
const createFetcher = useFetcherFactory();
|
|
1988
|
+
const selectMode = resolveSelectMode(options);
|
|
1989
|
+
const selectMinChars = resolveMinChars(options);
|
|
1990
|
+
const fetcherConfig = useMemo3(() => buildFetcherConfig(options), [options]);
|
|
1991
|
+
const fetcher = useMemo3(() => {
|
|
1992
|
+
if (!fetcherConfig) return null;
|
|
1993
|
+
return createFetcher(fetcherConfig);
|
|
1994
|
+
}, [createFetcher, fetcherConfig]);
|
|
1995
|
+
const queryKey = useMemo3(
|
|
1996
|
+
() => deriveQueryKey(fetcherConfig, name ?? ""),
|
|
1997
|
+
[fetcherConfig, name]
|
|
1998
|
+
);
|
|
1999
|
+
if (!fetcher) {
|
|
2000
|
+
return /* @__PURE__ */ jsxs12("div", { className: "text-sm text-destructive", children: [
|
|
2001
|
+
'EntitySelectWidget: missing endpoint or query in x-ui-options for field "',
|
|
2002
|
+
name,
|
|
2003
|
+
'"'
|
|
2004
|
+
] });
|
|
2005
|
+
}
|
|
2006
|
+
return /* @__PURE__ */ jsx19(
|
|
2007
|
+
EntitySelect,
|
|
2008
|
+
{
|
|
2009
|
+
id,
|
|
2010
|
+
fetcher,
|
|
2011
|
+
queryKey,
|
|
2012
|
+
mode: selectMode,
|
|
2013
|
+
minChars: selectMinChars,
|
|
2014
|
+
value: typeof value === "string" ? value : null,
|
|
2015
|
+
onChange: (v) => onChange(v ?? ""),
|
|
2016
|
+
onBlur: () => onBlur?.(id, value),
|
|
2017
|
+
label: label || false,
|
|
2018
|
+
description,
|
|
2019
|
+
placeholder,
|
|
2020
|
+
required,
|
|
2021
|
+
disabled: disabled || readonly,
|
|
2022
|
+
error: firstError(rawErrors)
|
|
2023
|
+
}
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/widgets/ObjectWidget.tsx
|
|
2028
|
+
import { useMemo as useMemo4 } from "react";
|
|
2029
|
+
|
|
2030
|
+
// src/hooks/useNestedFormContext.ts
|
|
2031
|
+
import { useContext } from "react";
|
|
2032
|
+
|
|
2033
|
+
// src/NestedFormContext.tsx
|
|
2034
|
+
import { createContext } from "react";
|
|
2035
|
+
var NestedFormContext = createContext(
|
|
2036
|
+
null
|
|
2037
|
+
);
|
|
2038
|
+
|
|
2039
|
+
// src/hooks/useNestedFormContext.ts
|
|
2040
|
+
function useNestedFormContext() {
|
|
2041
|
+
const ctx = useContext(NestedFormContext);
|
|
2042
|
+
if (!ctx) {
|
|
2043
|
+
throw new Error(
|
|
2044
|
+
"useNestedFormContext must be used inside a JsonSchemaFormRenderer."
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
return ctx;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// src/templates/DefaultObjectContainerTemplate.tsx
|
|
2051
|
+
import { useState as useState6 } from "react";
|
|
2052
|
+
import { ChevronDown as ChevronDown2, ChevronRight as ChevronRight2 } from "lucide-react";
|
|
2053
|
+
import { Card as Card2, CardContent, CardHeader, CardTitle } from "@petrarca/sonnet-ui";
|
|
2054
|
+
import {
|
|
2055
|
+
Collapsible as Collapsible2,
|
|
2056
|
+
CollapsibleContent as CollapsibleContent2,
|
|
2057
|
+
CollapsibleTrigger
|
|
2058
|
+
} from "@petrarca/sonnet-ui";
|
|
2059
|
+
import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2060
|
+
function CardVariant({
|
|
2061
|
+
children,
|
|
2062
|
+
label,
|
|
2063
|
+
description
|
|
2064
|
+
}) {
|
|
2065
|
+
return /* @__PURE__ */ jsxs13(Card2, { children: [
|
|
2066
|
+
label && /* @__PURE__ */ jsxs13(CardHeader, { className: "pb-3", children: [
|
|
2067
|
+
/* @__PURE__ */ jsx20(CardTitle, { className: "text-base", children: label }),
|
|
2068
|
+
description && /* @__PURE__ */ jsx20("p", { className: "text-sm text-muted-foreground", children: description })
|
|
2069
|
+
] }),
|
|
2070
|
+
/* @__PURE__ */ jsx20(CardContent, { children })
|
|
2071
|
+
] });
|
|
2072
|
+
}
|
|
2073
|
+
function SectionVariant({
|
|
2074
|
+
children,
|
|
2075
|
+
label,
|
|
2076
|
+
description
|
|
2077
|
+
}) {
|
|
2078
|
+
return /* @__PURE__ */ jsxs13("div", { className: "space-y-3", children: [
|
|
2079
|
+
label && /* @__PURE__ */ jsxs13("div", { children: [
|
|
2080
|
+
/* @__PURE__ */ jsx20("h4", { className: "text-sm font-medium", children: label }),
|
|
2081
|
+
description && /* @__PURE__ */ jsx20("p", { className: "text-xs text-muted-foreground", children: description })
|
|
2082
|
+
] }),
|
|
2083
|
+
/* @__PURE__ */ jsx20("div", { className: "pl-3 border-l-2 border-muted", children })
|
|
2084
|
+
] });
|
|
2085
|
+
}
|
|
2086
|
+
function DefaultObjectContainerTemplate({
|
|
2087
|
+
children,
|
|
2088
|
+
label,
|
|
2089
|
+
description,
|
|
2090
|
+
variant = "section",
|
|
2091
|
+
defaultOpen = true
|
|
2092
|
+
}) {
|
|
2093
|
+
switch (variant) {
|
|
2094
|
+
case "card":
|
|
2095
|
+
return /* @__PURE__ */ jsx20(CardVariant, { label, description, children });
|
|
2096
|
+
case "collapsible":
|
|
2097
|
+
if (!label) return /* @__PURE__ */ jsx20(SectionVariant, { children });
|
|
2098
|
+
return /* @__PURE__ */ jsx20(
|
|
2099
|
+
CollapsibleVariant,
|
|
2100
|
+
{
|
|
2101
|
+
label,
|
|
2102
|
+
description,
|
|
2103
|
+
defaultOpen,
|
|
2104
|
+
children
|
|
2105
|
+
}
|
|
2106
|
+
);
|
|
2107
|
+
case "inline":
|
|
2108
|
+
return /* @__PURE__ */ jsx20("div", { children });
|
|
2109
|
+
case "borderless":
|
|
2110
|
+
return /* @__PURE__ */ jsx20("div", { className: "space-y-4", children });
|
|
2111
|
+
case "section":
|
|
2112
|
+
default:
|
|
2113
|
+
return /* @__PURE__ */ jsx20(SectionVariant, { label, description, children });
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function CollapsibleVariant({
|
|
2117
|
+
label,
|
|
2118
|
+
description,
|
|
2119
|
+
defaultOpen,
|
|
2120
|
+
children
|
|
2121
|
+
}) {
|
|
2122
|
+
const [open, setOpen] = useState6(defaultOpen);
|
|
2123
|
+
return /* @__PURE__ */ jsxs13(Collapsible2, { open, onOpenChange: setOpen, children: [
|
|
2124
|
+
/* @__PURE__ */ jsx20(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsxs13(
|
|
2125
|
+
"button",
|
|
2126
|
+
{
|
|
2127
|
+
type: "button",
|
|
2128
|
+
className: "flex w-full items-center gap-2 text-sm font-medium hover:text-foreground/80",
|
|
2129
|
+
children: [
|
|
2130
|
+
open ? /* @__PURE__ */ jsx20(ChevronDown2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx20(ChevronRight2, { className: "h-4 w-4" }),
|
|
2131
|
+
label
|
|
2132
|
+
]
|
|
2133
|
+
}
|
|
2134
|
+
) }),
|
|
2135
|
+
description && /* @__PURE__ */ jsx20("p", { className: "text-xs text-muted-foreground ml-6", children: description }),
|
|
2136
|
+
/* @__PURE__ */ jsx20(CollapsibleContent2, { className: "mt-2 pl-3 border-l-2 border-muted", children })
|
|
2137
|
+
] });
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/widgets/ObjectWidget.tsx
|
|
2141
|
+
import { Fragment as Fragment3, jsx as jsx21 } from "react/jsx-runtime";
|
|
2142
|
+
function buildSubSchema(prop) {
|
|
2143
|
+
const properties = prop.properties;
|
|
2144
|
+
if (!properties) return null;
|
|
2145
|
+
return {
|
|
2146
|
+
title: prop.title || "",
|
|
2147
|
+
type: "object",
|
|
2148
|
+
properties,
|
|
2149
|
+
required: prop.required,
|
|
2150
|
+
...prop.allOf ? { allOf: prop.allOf } : {},
|
|
2151
|
+
...prop.if ? { if: prop.if } : {},
|
|
2152
|
+
...prop.then ? { then: prop.then } : {},
|
|
2153
|
+
...prop.else ? { else: prop.else } : {},
|
|
2154
|
+
...prop["x-ui-order"] ? { "x-ui-order": prop["x-ui-order"] } : {}
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
function buildEffectiveUiSchema(subUiSchema, globalOptions, layout) {
|
|
2158
|
+
return {
|
|
2159
|
+
...subUiSchema,
|
|
2160
|
+
...globalOptions ? { "x-ui-globalOptions": globalOptions } : {},
|
|
2161
|
+
...layout ? { "x-ui-layout": layout } : {}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function resolveObjectOptions(options) {
|
|
2165
|
+
return {
|
|
2166
|
+
variant: options?.variant || "section",
|
|
2167
|
+
defaultOpen: options?.defaultOpen !== false,
|
|
2168
|
+
displayContents: options?.displayContents ?? false,
|
|
2169
|
+
globalOptions: options?.globalOptions
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function resolveLayout(uiSchema, propertySchema) {
|
|
2173
|
+
return uiSchema?.["x-ui-layout"] ?? propertySchema["x-ui-layout"];
|
|
2174
|
+
}
|
|
2175
|
+
function ObjectWidget({
|
|
2176
|
+
propertySchema,
|
|
2177
|
+
value,
|
|
2178
|
+
onChange,
|
|
2179
|
+
registry,
|
|
2180
|
+
uiSchema,
|
|
2181
|
+
disabled,
|
|
2182
|
+
readonly,
|
|
2183
|
+
label,
|
|
2184
|
+
description,
|
|
2185
|
+
options
|
|
2186
|
+
}) {
|
|
2187
|
+
const { renderer: Renderer, templates } = useNestedFormContext();
|
|
2188
|
+
const ContainerTemplate = templates.ObjectContainerTemplate ?? DefaultObjectContainerTemplate;
|
|
2189
|
+
const subSchema = useMemo4(
|
|
2190
|
+
() => buildSubSchema(propertySchema),
|
|
2191
|
+
[propertySchema]
|
|
2192
|
+
);
|
|
2193
|
+
const subUiSchema = useMemo4(
|
|
2194
|
+
() => uiSchema?.["x-ui-fields"] ?? {},
|
|
2195
|
+
[uiSchema]
|
|
2196
|
+
);
|
|
2197
|
+
if (!subSchema) return null;
|
|
2198
|
+
const { variant, defaultOpen, displayContents, globalOptions } = resolveObjectOptions(options);
|
|
2199
|
+
const layout = resolveLayout(uiSchema, propertySchema);
|
|
2200
|
+
const effectiveUiSchema = buildEffectiveUiSchema(
|
|
2201
|
+
subUiSchema,
|
|
2202
|
+
globalOptions,
|
|
2203
|
+
layout
|
|
2204
|
+
);
|
|
2205
|
+
const content = /* @__PURE__ */ jsx21(
|
|
2206
|
+
Renderer,
|
|
2207
|
+
{
|
|
2208
|
+
schema: subSchema,
|
|
2209
|
+
data: value || {},
|
|
2210
|
+
onChange,
|
|
2211
|
+
disabled,
|
|
2212
|
+
readOnly: readonly,
|
|
2213
|
+
widgets: registry,
|
|
2214
|
+
uiSchema: effectiveUiSchema,
|
|
2215
|
+
displayContents
|
|
2216
|
+
}
|
|
2217
|
+
);
|
|
2218
|
+
if (displayContents) return /* @__PURE__ */ jsx21(Fragment3, { children: content });
|
|
2219
|
+
return /* @__PURE__ */ jsx21(
|
|
2220
|
+
ContainerTemplate,
|
|
2221
|
+
{
|
|
2222
|
+
label,
|
|
2223
|
+
description,
|
|
2224
|
+
variant,
|
|
2225
|
+
defaultOpen,
|
|
2226
|
+
children: content
|
|
2227
|
+
}
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// src/widgets/ArrayWidget.tsx
|
|
2232
|
+
import { useCallback as useCallback5, useMemo as useMemo5 } from "react";
|
|
2233
|
+
import { Plus } from "lucide-react";
|
|
2234
|
+
import { Button as Button5 } from "@petrarca/sonnet-ui";
|
|
2235
|
+
import { applySchemaDefaults } from "@petrarca/sonnet-core/schema";
|
|
2236
|
+
import { FORM_ITEM_ID_FIELD, generateId } from "@petrarca/sonnet-core";
|
|
2237
|
+
|
|
2238
|
+
// src/templates/DefaultArrayItemTemplate.tsx
|
|
2239
|
+
import { Trash2, ChevronUp, ChevronDown as ChevronDown3 } from "lucide-react";
|
|
2240
|
+
import { Button as Button4 } from "@petrarca/sonnet-ui";
|
|
2241
|
+
import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2242
|
+
function DefaultArrayItemTemplate({
|
|
2243
|
+
children,
|
|
2244
|
+
index,
|
|
2245
|
+
total,
|
|
2246
|
+
canRemove,
|
|
2247
|
+
orderable,
|
|
2248
|
+
disabled,
|
|
2249
|
+
onRemove,
|
|
2250
|
+
onMoveUp,
|
|
2251
|
+
onMoveDown
|
|
2252
|
+
}) {
|
|
2253
|
+
const showControls = canRemove || orderable;
|
|
2254
|
+
return /* @__PURE__ */ jsxs14("div", { className: "relative flex gap-2 rounded-md border border-border/50 bg-muted/20 p-3", children: [
|
|
2255
|
+
/* @__PURE__ */ jsx22("div", { className: "flex-1 min-w-0", children }),
|
|
2256
|
+
showControls && /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1 pt-0.5", children: [
|
|
2257
|
+
orderable && index > 0 && /* @__PURE__ */ jsx22(
|
|
2258
|
+
Button4,
|
|
2259
|
+
{
|
|
2260
|
+
type: "button",
|
|
2261
|
+
variant: "ghost",
|
|
2262
|
+
size: "compact",
|
|
2263
|
+
disabled,
|
|
2264
|
+
onClick: onMoveUp,
|
|
2265
|
+
title: "Move up",
|
|
2266
|
+
children: /* @__PURE__ */ jsx22(ChevronUp, { className: "h-3.5 w-3.5" })
|
|
2267
|
+
}
|
|
2268
|
+
),
|
|
2269
|
+
orderable && index < total - 1 && /* @__PURE__ */ jsx22(
|
|
2270
|
+
Button4,
|
|
2271
|
+
{
|
|
2272
|
+
type: "button",
|
|
2273
|
+
variant: "ghost",
|
|
2274
|
+
size: "compact",
|
|
2275
|
+
disabled,
|
|
2276
|
+
onClick: onMoveDown,
|
|
2277
|
+
title: "Move down",
|
|
2278
|
+
children: /* @__PURE__ */ jsx22(ChevronDown3, { className: "h-3.5 w-3.5" })
|
|
2279
|
+
}
|
|
2280
|
+
),
|
|
2281
|
+
canRemove && /* @__PURE__ */ jsx22(
|
|
2282
|
+
Button4,
|
|
2283
|
+
{
|
|
2284
|
+
type: "button",
|
|
2285
|
+
variant: "ghost",
|
|
2286
|
+
size: "compact",
|
|
2287
|
+
disabled,
|
|
2288
|
+
onClick: onRemove,
|
|
2289
|
+
title: "Remove item",
|
|
2290
|
+
className: "text-muted-foreground hover:text-destructive",
|
|
2291
|
+
children: /* @__PURE__ */ jsx22(Trash2, { className: "h-3.5 w-3.5" })
|
|
2292
|
+
}
|
|
2293
|
+
)
|
|
2294
|
+
] })
|
|
2295
|
+
] });
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/templates/gridUtils.ts
|
|
2299
|
+
function buildGridTemplate(layout, canRemove, fieldCount, orderable = false) {
|
|
2300
|
+
const base = layout.columns ?? `repeat(${fieldCount}, 1fr)`;
|
|
2301
|
+
const withOrder = orderable ? `${base} 32px` : base;
|
|
2302
|
+
return canRemove ? `${withOrder} 32px` : withOrder;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/templates/DefaultArrayHeaderTemplate.tsx
|
|
2306
|
+
import { jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2307
|
+
function DefaultArrayHeaderTemplate({
|
|
2308
|
+
itemSchema,
|
|
2309
|
+
layout,
|
|
2310
|
+
canRemove,
|
|
2311
|
+
orderable,
|
|
2312
|
+
fieldCount
|
|
2313
|
+
}) {
|
|
2314
|
+
if (layout.direction !== "horizontal") return null;
|
|
2315
|
+
const template = buildGridTemplate(layout, canRemove, fieldCount, orderable);
|
|
2316
|
+
const gap = layout.gap ?? 8;
|
|
2317
|
+
const properties = itemSchema.properties ?? {};
|
|
2318
|
+
const order = itemSchema["x-ui-order"];
|
|
2319
|
+
const fieldNames = order ? order.filter((k) => k in properties) : Object.keys(properties);
|
|
2320
|
+
return /* @__PURE__ */ jsxs15(
|
|
2321
|
+
"div",
|
|
2322
|
+
{
|
|
2323
|
+
style: {
|
|
2324
|
+
display: "grid",
|
|
2325
|
+
gridTemplateColumns: template,
|
|
2326
|
+
gap: `${gap}px`
|
|
2327
|
+
},
|
|
2328
|
+
children: [
|
|
2329
|
+
fieldNames.map((name) => /* @__PURE__ */ jsx23("span", { className: "text-xs font-medium text-muted-foreground", children: properties[name]?.title ?? name }, name)),
|
|
2330
|
+
orderable && /* @__PURE__ */ jsx23("span", {}),
|
|
2331
|
+
canRemove && /* @__PURE__ */ jsx23("span", {})
|
|
2332
|
+
]
|
|
2333
|
+
}
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/widgets/ArrayWidget.tsx
|
|
2338
|
+
import { jsx as jsx24, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2339
|
+
function countFields(itemSchema) {
|
|
2340
|
+
return Object.keys(itemSchema.properties ?? {}).length;
|
|
2341
|
+
}
|
|
2342
|
+
function allItemsComplete(items, requiredFields) {
|
|
2343
|
+
if (requiredFields.length === 0) return true;
|
|
2344
|
+
return items.every(
|
|
2345
|
+
(item) => requiredFields.every((field) => {
|
|
2346
|
+
const v = item[field];
|
|
2347
|
+
return v !== void 0 && v !== null && v !== "";
|
|
2348
|
+
})
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
function ensureItemIds(value) {
|
|
2352
|
+
return (Array.isArray(value) ? value : []).map(
|
|
2353
|
+
(item) => item[FORM_ITEM_ID_FIELD] ? item : { ...item, [FORM_ITEM_ID_FIELD]: generateId() }
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
function stripItemId(item) {
|
|
2357
|
+
return Object.fromEntries(
|
|
2358
|
+
Object.entries(item).filter(([k]) => k !== FORM_ITEM_ID_FIELD)
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
function resolveLayout2(uiSchema, propertySchema) {
|
|
2362
|
+
const schemaLayout = propertySchema["x-ui-layout"];
|
|
2363
|
+
const uiLayout = uiSchema?.["x-ui-layout"];
|
|
2364
|
+
return { ...schemaLayout, ...uiLayout };
|
|
2365
|
+
}
|
|
2366
|
+
function buildSubGlobalOptions(layout, isHorizontal) {
|
|
2367
|
+
const compactOptions = layout.compact ? { compact: true } : void 0;
|
|
2368
|
+
const labelsOption = layout.labels !== void 0 ? layout.labels : isHorizontal ? false : void 0;
|
|
2369
|
+
if (!compactOptions && labelsOption !== false) return void 0;
|
|
2370
|
+
return {
|
|
2371
|
+
...compactOptions ?? {},
|
|
2372
|
+
...labelsOption === false ? { label: false } : {}
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
function ArrayHeader({ label, description }) {
|
|
2376
|
+
if (!label) return null;
|
|
2377
|
+
return /* @__PURE__ */ jsxs16("div", { children: [
|
|
2378
|
+
/* @__PURE__ */ jsx24("h4", { className: "text-sm font-medium", children: label }),
|
|
2379
|
+
description && /* @__PURE__ */ jsx24("p", { className: "text-xs text-muted-foreground", children: description })
|
|
2380
|
+
] });
|
|
2381
|
+
}
|
|
2382
|
+
function useArrayHandlers(items, onChange, propertySchema) {
|
|
2383
|
+
const handleItemChange = useCallback5(
|
|
2384
|
+
(index, data) => {
|
|
2385
|
+
const next = [...items];
|
|
2386
|
+
next[index] = {
|
|
2387
|
+
...data,
|
|
2388
|
+
[FORM_ITEM_ID_FIELD]: items[index][FORM_ITEM_ID_FIELD]
|
|
2389
|
+
};
|
|
2390
|
+
onChange(next);
|
|
2391
|
+
},
|
|
2392
|
+
[items, onChange]
|
|
2393
|
+
);
|
|
2394
|
+
const handleAdd = useCallback5(() => {
|
|
2395
|
+
const base = propertySchema.items ? applySchemaDefaults({}, propertySchema.items) : {};
|
|
2396
|
+
onChange([...items, { ...base, [FORM_ITEM_ID_FIELD]: generateId() }]);
|
|
2397
|
+
}, [items, onChange, propertySchema.items]);
|
|
2398
|
+
const handleRemove = useCallback5(
|
|
2399
|
+
(index) => {
|
|
2400
|
+
onChange(items.filter((_, i) => i !== index));
|
|
2401
|
+
},
|
|
2402
|
+
[items, onChange]
|
|
2403
|
+
);
|
|
2404
|
+
const handleMove = useCallback5(
|
|
2405
|
+
(from, to) => {
|
|
2406
|
+
const next = [...items];
|
|
2407
|
+
const [movedItem] = next.splice(from, 1);
|
|
2408
|
+
next.splice(to, 0, movedItem);
|
|
2409
|
+
onChange(next);
|
|
2410
|
+
},
|
|
2411
|
+
[items, onChange]
|
|
2412
|
+
);
|
|
2413
|
+
return { handleItemChange, handleAdd, handleRemove, handleMove };
|
|
2414
|
+
}
|
|
2415
|
+
function ArrayWidget({
|
|
2416
|
+
propertySchema,
|
|
2417
|
+
value,
|
|
2418
|
+
onChange,
|
|
2419
|
+
registry,
|
|
2420
|
+
uiSchema,
|
|
2421
|
+
disabled,
|
|
2422
|
+
readonly,
|
|
2423
|
+
label,
|
|
2424
|
+
description,
|
|
2425
|
+
options,
|
|
2426
|
+
schema,
|
|
2427
|
+
name
|
|
2428
|
+
}) {
|
|
2429
|
+
const { templates } = useNestedFormContext();
|
|
2430
|
+
const items = useMemo5(() => ensureItemIds(value), [value]);
|
|
2431
|
+
const { handleItemChange, handleAdd, handleRemove, handleMove } = useArrayHandlers(items, onChange, propertySchema);
|
|
2432
|
+
const itemSchema = propertySchema.items;
|
|
2433
|
+
if (!itemSchema?.properties) {
|
|
2434
|
+
return /* @__PURE__ */ jsx24("p", { className: "text-sm text-destructive", children: 'ArrayWidget requires items with type "object" and properties.' });
|
|
2435
|
+
}
|
|
2436
|
+
const layout = resolveLayout2(uiSchema, propertySchema);
|
|
2437
|
+
const isHorizontal = layout.direction === "horizontal";
|
|
2438
|
+
const subGlobalOptions = buildSubGlobalOptions(layout, isHorizontal);
|
|
2439
|
+
return /* @__PURE__ */ jsx24(
|
|
2440
|
+
ArrayWidgetInner,
|
|
2441
|
+
{
|
|
2442
|
+
items,
|
|
2443
|
+
propertySchema,
|
|
2444
|
+
itemSchema,
|
|
2445
|
+
schema,
|
|
2446
|
+
name: name ?? "",
|
|
2447
|
+
registry,
|
|
2448
|
+
uiSchema,
|
|
2449
|
+
disabled,
|
|
2450
|
+
readonly,
|
|
2451
|
+
label,
|
|
2452
|
+
description,
|
|
2453
|
+
options,
|
|
2454
|
+
layout,
|
|
2455
|
+
isHorizontal,
|
|
2456
|
+
subGlobalOptions,
|
|
2457
|
+
templates,
|
|
2458
|
+
onItemChange: handleItemChange,
|
|
2459
|
+
onAdd: handleAdd,
|
|
2460
|
+
onRemove: handleRemove,
|
|
2461
|
+
onMove: handleMove
|
|
2462
|
+
}
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
function resolveConstraints(arraySchema, itemSchema, readonly, disabled, itemCount) {
|
|
2466
|
+
const minItems = arraySchema.minItems ?? 0;
|
|
2467
|
+
const maxItems = arraySchema.maxItems ?? Infinity;
|
|
2468
|
+
const requiredFields = itemSchema.required ?? [];
|
|
2469
|
+
const isEditable = !readonly && !disabled;
|
|
2470
|
+
return {
|
|
2471
|
+
canRemove: isEditable && itemCount > minItems,
|
|
2472
|
+
canAddBase: isEditable && itemCount < maxItems,
|
|
2473
|
+
requiredFields
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
function resolveDisplayOptions(options, itemCount) {
|
|
2477
|
+
return {
|
|
2478
|
+
addLabel: options?.addLabel ?? "Add item",
|
|
2479
|
+
itemTitle: options?.itemTitle,
|
|
2480
|
+
orderable: !!options?.orderable && itemCount > 1,
|
|
2481
|
+
emptyMessage: options?.emptyMessage
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
function resolveTemplates(templates, isHorizontal) {
|
|
2485
|
+
const ItemTemplate = templates.ArrayItemTemplate ?? DefaultArrayItemTemplate;
|
|
2486
|
+
const HorizontalItemTemplate = templates.HorizontalArrayItemTemplate ?? void 0;
|
|
2487
|
+
const HeaderTemplate = templates.ArrayHeaderTemplate ?? DefaultArrayHeaderTemplate;
|
|
2488
|
+
const ActiveItemTemplate = isHorizontal && HorizontalItemTemplate ? HorizontalItemTemplate : ItemTemplate;
|
|
2489
|
+
return { ActiveItemTemplate, HeaderTemplate };
|
|
2490
|
+
}
|
|
2491
|
+
function buildItemOptions(isHorizontal, subGlobalOptions) {
|
|
2492
|
+
return {
|
|
2493
|
+
variant: "borderless",
|
|
2494
|
+
displayContents: isHorizontal,
|
|
2495
|
+
...subGlobalOptions ? { globalOptions: subGlobalOptions } : {}
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
function resolveItemLabel(itemTitle, index) {
|
|
2499
|
+
return itemTitle ? itemTitle.replace("{index}", String(index + 1)) : "";
|
|
2500
|
+
}
|
|
2501
|
+
function ArrayWidgetInner({
|
|
2502
|
+
items,
|
|
2503
|
+
propertySchema,
|
|
2504
|
+
itemSchema,
|
|
2505
|
+
schema,
|
|
2506
|
+
name,
|
|
2507
|
+
registry,
|
|
2508
|
+
uiSchema,
|
|
2509
|
+
disabled,
|
|
2510
|
+
readonly,
|
|
2511
|
+
label,
|
|
2512
|
+
description,
|
|
2513
|
+
options,
|
|
2514
|
+
layout,
|
|
2515
|
+
isHorizontal,
|
|
2516
|
+
subGlobalOptions,
|
|
2517
|
+
templates,
|
|
2518
|
+
onItemChange,
|
|
2519
|
+
onAdd,
|
|
2520
|
+
onRemove,
|
|
2521
|
+
onMove
|
|
2522
|
+
}) {
|
|
2523
|
+
const constraints = resolveConstraints(
|
|
2524
|
+
propertySchema,
|
|
2525
|
+
itemSchema,
|
|
2526
|
+
readonly,
|
|
2527
|
+
disabled,
|
|
2528
|
+
items.length
|
|
2529
|
+
);
|
|
2530
|
+
const display = resolveDisplayOptions(options, items.length);
|
|
2531
|
+
const canAdd = constraints.canAddBase && allItemsComplete(items, constraints.requiredFields);
|
|
2532
|
+
const { ActiveItemTemplate, HeaderTemplate } = resolveTemplates(
|
|
2533
|
+
templates,
|
|
2534
|
+
isHorizontal
|
|
2535
|
+
);
|
|
2536
|
+
const fieldCount = countFields(itemSchema);
|
|
2537
|
+
const itemOptions = buildItemOptions(isHorizontal, subGlobalOptions);
|
|
2538
|
+
return /* @__PURE__ */ jsxs16("div", { className: isHorizontal ? "space-y-1" : "space-y-3", children: [
|
|
2539
|
+
/* @__PURE__ */ jsx24(ArrayHeader, { label, description }),
|
|
2540
|
+
items.length === 0 && display.emptyMessage && /* @__PURE__ */ jsx24("p", { className: "text-sm text-muted-foreground italic", children: display.emptyMessage }),
|
|
2541
|
+
items.length > 0 && /* @__PURE__ */ jsx24(
|
|
2542
|
+
HeaderTemplate,
|
|
2543
|
+
{
|
|
2544
|
+
itemSchema,
|
|
2545
|
+
layout,
|
|
2546
|
+
canRemove: constraints.canRemove,
|
|
2547
|
+
orderable: display.orderable,
|
|
2548
|
+
fieldCount
|
|
2549
|
+
}
|
|
2550
|
+
),
|
|
2551
|
+
items.map((item, index) => /* @__PURE__ */ jsx24(
|
|
2552
|
+
ActiveItemTemplate,
|
|
2553
|
+
{
|
|
2554
|
+
index,
|
|
2555
|
+
total: items.length,
|
|
2556
|
+
canRemove: constraints.canRemove,
|
|
2557
|
+
orderable: display.orderable,
|
|
2558
|
+
disabled,
|
|
2559
|
+
layout,
|
|
2560
|
+
fieldCount,
|
|
2561
|
+
onRemove: () => onRemove(index),
|
|
2562
|
+
onMoveUp: index > 0 ? () => onMove(index, index - 1) : void 0,
|
|
2563
|
+
onMoveDown: index < items.length - 1 ? () => onMove(index, index + 1) : void 0,
|
|
2564
|
+
children: /* @__PURE__ */ jsx24(
|
|
2565
|
+
ObjectWidget,
|
|
2566
|
+
{
|
|
2567
|
+
id: `field-${name}-${index}`,
|
|
2568
|
+
name: `${name}[${index}]`,
|
|
2569
|
+
schema,
|
|
2570
|
+
propertySchema: itemSchema,
|
|
2571
|
+
uiSchema,
|
|
2572
|
+
registry,
|
|
2573
|
+
value: stripItemId(item),
|
|
2574
|
+
onChange: (data) => onItemChange(index, data),
|
|
2575
|
+
label: resolveItemLabel(display.itemTitle, index),
|
|
2576
|
+
description: void 0,
|
|
2577
|
+
disabled,
|
|
2578
|
+
readonly,
|
|
2579
|
+
options: itemOptions
|
|
2580
|
+
}
|
|
2581
|
+
)
|
|
2582
|
+
},
|
|
2583
|
+
item[FORM_ITEM_ID_FIELD]
|
|
2584
|
+
)),
|
|
2585
|
+
canAdd && /* @__PURE__ */ jsxs16(
|
|
2586
|
+
Button5,
|
|
2587
|
+
{
|
|
2588
|
+
type: "button",
|
|
2589
|
+
variant: "outline",
|
|
2590
|
+
size: "sm",
|
|
2591
|
+
onClick: onAdd,
|
|
2592
|
+
disabled,
|
|
2593
|
+
className: "gap-1.5",
|
|
2594
|
+
children: [
|
|
2595
|
+
/* @__PURE__ */ jsx24(Plus, { className: "h-3.5 w-3.5" }),
|
|
2596
|
+
display.addLabel
|
|
2597
|
+
]
|
|
2598
|
+
}
|
|
2599
|
+
)
|
|
2600
|
+
] });
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// src/widgets/QuantityWidget.tsx
|
|
2604
|
+
import { useCallback as useCallback6, useRef as useRef4 } from "react";
|
|
2605
|
+
import { jsx as jsx25 } from "react/jsx-runtime";
|
|
2606
|
+
function isUnitOption(u) {
|
|
2607
|
+
return typeof u === "object" && u !== null && typeof u.value === "string" && typeof u.label === "string";
|
|
2608
|
+
}
|
|
2609
|
+
function QuantityWidget({
|
|
2610
|
+
id,
|
|
2611
|
+
name,
|
|
2612
|
+
value,
|
|
2613
|
+
onChange,
|
|
2614
|
+
onBlur,
|
|
2615
|
+
propertySchema,
|
|
2616
|
+
required,
|
|
2617
|
+
disabled,
|
|
2618
|
+
readonly,
|
|
2619
|
+
rawErrors,
|
|
2620
|
+
label,
|
|
2621
|
+
description,
|
|
2622
|
+
placeholder,
|
|
2623
|
+
autofocus,
|
|
2624
|
+
options = {}
|
|
2625
|
+
}) {
|
|
2626
|
+
const error = firstError(rawErrors);
|
|
2627
|
+
const units = Array.isArray(options.units) ? options.units.filter(isUnitOption) : [];
|
|
2628
|
+
const precision = typeof options.precision === "number" ? options.precision : void 0;
|
|
2629
|
+
const step = typeof options.step === "number" ? options.step : void 0;
|
|
2630
|
+
const unitPositionOpt = options.unitPosition === "prefix" || options.unitPosition === "suffix" ? options.unitPosition : void 0;
|
|
2631
|
+
const quantityValue = value != null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2632
|
+
const valueRef = useRef4(value);
|
|
2633
|
+
valueRef.current = value;
|
|
2634
|
+
const handleBlur = useCallback6(() => {
|
|
2635
|
+
onBlur?.(id, valueRef.current);
|
|
2636
|
+
}, [id, onBlur]);
|
|
2637
|
+
return /* @__PURE__ */ jsx25(
|
|
2638
|
+
FormQuantityInput,
|
|
2639
|
+
{
|
|
2640
|
+
id,
|
|
2641
|
+
name,
|
|
2642
|
+
value: quantityValue,
|
|
2643
|
+
onChange,
|
|
2644
|
+
onBlur: handleBlur,
|
|
2645
|
+
units,
|
|
2646
|
+
unitPosition: unitPositionOpt,
|
|
2647
|
+
precision,
|
|
2648
|
+
step,
|
|
2649
|
+
min: propertySchema.minimum,
|
|
2650
|
+
max: propertySchema.maximum,
|
|
2651
|
+
label,
|
|
2652
|
+
description,
|
|
2653
|
+
error,
|
|
2654
|
+
placeholder,
|
|
2655
|
+
required,
|
|
2656
|
+
disabled,
|
|
2657
|
+
readOnly: readonly,
|
|
2658
|
+
autoFocus: autofocus
|
|
2659
|
+
}
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// src/widgets/widgetResolver.ts
|
|
2664
|
+
import {
|
|
2665
|
+
isNullableBoolean as isNullableBoolean2,
|
|
2666
|
+
extractEnumValues as extractEnumValues2,
|
|
2667
|
+
getFieldType
|
|
2668
|
+
} from "@petrarca/sonnet-core/schema";
|
|
2669
|
+
var DEFAULT_WIDGET_MAP = {
|
|
2670
|
+
string: {
|
|
2671
|
+
default: "TextInputWidget",
|
|
2672
|
+
textarea: "TextareaWidget",
|
|
2673
|
+
email: "TextInputWidget",
|
|
2674
|
+
// Uses email input type
|
|
2675
|
+
uri: "TextInputWidget",
|
|
2676
|
+
// Uses url input type
|
|
2677
|
+
url: "TextInputWidget"
|
|
2678
|
+
// Uses url input type
|
|
2679
|
+
},
|
|
2680
|
+
number: {
|
|
2681
|
+
default: "NumberWidget"
|
|
2682
|
+
},
|
|
2683
|
+
integer: {
|
|
2684
|
+
default: "NumberWidget"
|
|
2685
|
+
// NumberWidget handles both number and integer
|
|
2686
|
+
},
|
|
2687
|
+
boolean: {
|
|
2688
|
+
default: "CheckboxWidget"
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
function resolveWidgetByName(widgetName, propertySchema, registry) {
|
|
2692
|
+
if (registry[widgetName]) {
|
|
2693
|
+
return registry[widgetName];
|
|
2694
|
+
}
|
|
2695
|
+
const type = getFieldType(propertySchema);
|
|
2696
|
+
const resolvedWidgetName = DEFAULT_WIDGET_MAP[type]?.[widgetName.toLowerCase()];
|
|
2697
|
+
if (resolvedWidgetName && registry[resolvedWidgetName]) {
|
|
2698
|
+
return registry[resolvedWidgetName];
|
|
2699
|
+
}
|
|
2700
|
+
return null;
|
|
2701
|
+
}
|
|
2702
|
+
function mergeUiSchema(propertySchema, uiSchema) {
|
|
2703
|
+
const xWidgetName = propertySchema["x-ui-widget"];
|
|
2704
|
+
const xWidgetOptions = propertySchema["x-ui-options"];
|
|
2705
|
+
if (!xWidgetName && !xWidgetOptions) return uiSchema;
|
|
2706
|
+
if (!uiSchema) {
|
|
2707
|
+
return {
|
|
2708
|
+
...xWidgetName && { "x-ui-widget": xWidgetName },
|
|
2709
|
+
...xWidgetOptions && { "x-ui-options": xWidgetOptions }
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
return {
|
|
2713
|
+
...xWidgetName && { "x-ui-widget": xWidgetName },
|
|
2714
|
+
// lower priority
|
|
2715
|
+
...uiSchema,
|
|
2716
|
+
// higher priority
|
|
2717
|
+
"x-ui-options": {
|
|
2718
|
+
...xWidgetOptions || {},
|
|
2719
|
+
// lower priority
|
|
2720
|
+
...uiSchema["x-ui-options"] || {}
|
|
2721
|
+
// higher priority
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
function resolveFromUiWidget(uiWidget, propertySchema, registry) {
|
|
2726
|
+
if (typeof uiWidget === "function") return uiWidget;
|
|
2727
|
+
if (typeof uiWidget === "string")
|
|
2728
|
+
return resolveWidgetByName(uiWidget, propertySchema, registry);
|
|
2729
|
+
return null;
|
|
2730
|
+
}
|
|
2731
|
+
function isObjectArraySchema(schema) {
|
|
2732
|
+
return schema.type === "array" && schema.items?.type === "object" && !!schema.items?.properties;
|
|
2733
|
+
}
|
|
2734
|
+
function registryLookup(registry, widgetName) {
|
|
2735
|
+
return registry[widgetName] || registry["TextInputWidget"];
|
|
2736
|
+
}
|
|
2737
|
+
function resolveFromSchemaStructure(schema, registry) {
|
|
2738
|
+
if (isNullableBoolean2(schema))
|
|
2739
|
+
return registryLookup(registry, "CheckboxWidget");
|
|
2740
|
+
if (extractEnumValues2(schema).length > 0)
|
|
2741
|
+
return registryLookup(registry, "SelectWidget");
|
|
2742
|
+
if (schema.type === "object" && schema.properties)
|
|
2743
|
+
return registryLookup(registry, "ObjectWidget");
|
|
2744
|
+
if (isObjectArraySchema(schema))
|
|
2745
|
+
return registryLookup(registry, "ArrayWidget");
|
|
2746
|
+
return null;
|
|
2747
|
+
}
|
|
2748
|
+
function resolveFromTypeFormat(schema, registry) {
|
|
2749
|
+
const type = getFieldType(schema);
|
|
2750
|
+
const format = schema.format;
|
|
2751
|
+
const widgetName = format && DEFAULT_WIDGET_MAP[type]?.[format] || DEFAULT_WIDGET_MAP[type]?.default || "TextInputWidget";
|
|
2752
|
+
return registry[widgetName] || registry["TextInputWidget"];
|
|
2753
|
+
}
|
|
2754
|
+
function resolveWidget(propertySchema, uiSchema, registry) {
|
|
2755
|
+
const effectiveUiSchema = mergeUiSchema(propertySchema, uiSchema);
|
|
2756
|
+
const uiWidget = effectiveUiSchema?.["x-ui-widget"];
|
|
2757
|
+
if (uiWidget) {
|
|
2758
|
+
const resolved = resolveFromUiWidget(uiWidget, propertySchema, registry);
|
|
2759
|
+
if (resolved) return resolved;
|
|
2760
|
+
}
|
|
2761
|
+
return resolveFromSchemaStructure(propertySchema, registry) ?? resolveFromTypeFormat(propertySchema, registry);
|
|
2762
|
+
}
|
|
2763
|
+
function getDefaultWidgetForType(type) {
|
|
2764
|
+
return DEFAULT_WIDGET_MAP[type]?.default || "TextInputWidget";
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// src/widgets/index.ts
|
|
2768
|
+
var DEFAULT_WIDGETS = {
|
|
2769
|
+
// Canonical PascalCase names
|
|
2770
|
+
TextInputWidget,
|
|
2771
|
+
TextareaWidget,
|
|
2772
|
+
NumberWidget,
|
|
2773
|
+
CheckboxWidget,
|
|
2774
|
+
SelectWidget,
|
|
2775
|
+
TagsInputWidget,
|
|
2776
|
+
JsonEditorWidget,
|
|
2777
|
+
EntitySelectWidget,
|
|
2778
|
+
ObjectWidget,
|
|
2779
|
+
ArrayWidget,
|
|
2780
|
+
QuantityWidget,
|
|
2781
|
+
// Lowercase aliases for x-ui-widget convenience
|
|
2782
|
+
text: TextInputWidget,
|
|
2783
|
+
textarea: TextareaWidget,
|
|
2784
|
+
number: NumberWidget,
|
|
2785
|
+
checkbox: CheckboxWidget,
|
|
2786
|
+
select: SelectWidget,
|
|
2787
|
+
tags: TagsInputWidget,
|
|
2788
|
+
"json-editor": JsonEditorWidget,
|
|
2789
|
+
"entity-select": EntitySelectWidget,
|
|
2790
|
+
object: ObjectWidget,
|
|
2791
|
+
array: ArrayWidget,
|
|
2792
|
+
quantity: QuantityWidget
|
|
2793
|
+
};
|
|
2794
|
+
|
|
2795
|
+
// src/templates/HorizontalArrayItemTemplate.tsx
|
|
2796
|
+
import { Trash2 as Trash22, ChevronUp as ChevronUp2, ChevronDown as ChevronDown4 } from "lucide-react";
|
|
2797
|
+
import { Button as Button6 } from "@petrarca/sonnet-ui";
|
|
2798
|
+
import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2799
|
+
function HorizontalArrayItemTemplate({
|
|
2800
|
+
children,
|
|
2801
|
+
index,
|
|
2802
|
+
total,
|
|
2803
|
+
canRemove,
|
|
2804
|
+
orderable,
|
|
2805
|
+
disabled,
|
|
2806
|
+
layout,
|
|
2807
|
+
fieldCount,
|
|
2808
|
+
onRemove,
|
|
2809
|
+
onMoveUp,
|
|
2810
|
+
onMoveDown
|
|
2811
|
+
}) {
|
|
2812
|
+
const resolvedLayout = layout;
|
|
2813
|
+
const template = buildGridTemplate(
|
|
2814
|
+
resolvedLayout,
|
|
2815
|
+
canRemove,
|
|
2816
|
+
fieldCount,
|
|
2817
|
+
orderable
|
|
2818
|
+
);
|
|
2819
|
+
const gap = resolvedLayout.gap ?? 8;
|
|
2820
|
+
const alignItems = resolvedLayout.compact ? "center" : "start";
|
|
2821
|
+
return /* @__PURE__ */ jsxs17(
|
|
2822
|
+
"div",
|
|
2823
|
+
{
|
|
2824
|
+
style: {
|
|
2825
|
+
display: "grid",
|
|
2826
|
+
gridTemplateColumns: template,
|
|
2827
|
+
gap: `${gap}px`,
|
|
2828
|
+
alignItems
|
|
2829
|
+
},
|
|
2830
|
+
children: [
|
|
2831
|
+
children,
|
|
2832
|
+
orderable && /* @__PURE__ */ jsxs17("div", { className: "flex flex-col items-center justify-center", children: [
|
|
2833
|
+
/* @__PURE__ */ jsx26(
|
|
2834
|
+
Button6,
|
|
2835
|
+
{
|
|
2836
|
+
type: "button",
|
|
2837
|
+
variant: "ghost",
|
|
2838
|
+
size: "compact",
|
|
2839
|
+
disabled: disabled || index === 0,
|
|
2840
|
+
onClick: onMoveUp,
|
|
2841
|
+
title: "Move up",
|
|
2842
|
+
className: "h-4 w-8 p-0",
|
|
2843
|
+
children: /* @__PURE__ */ jsx26(ChevronUp2, { className: "h-3 w-3" })
|
|
2844
|
+
}
|
|
2845
|
+
),
|
|
2846
|
+
/* @__PURE__ */ jsx26(
|
|
2847
|
+
Button6,
|
|
2848
|
+
{
|
|
2849
|
+
type: "button",
|
|
2850
|
+
variant: "ghost",
|
|
2851
|
+
size: "compact",
|
|
2852
|
+
disabled: disabled || index >= total - 1,
|
|
2853
|
+
onClick: onMoveDown,
|
|
2854
|
+
title: "Move down",
|
|
2855
|
+
className: "h-4 w-8 p-0",
|
|
2856
|
+
children: /* @__PURE__ */ jsx26(ChevronDown4, { className: "h-3 w-3" })
|
|
2857
|
+
}
|
|
2858
|
+
)
|
|
2859
|
+
] }),
|
|
2860
|
+
canRemove && /* @__PURE__ */ jsx26(
|
|
2861
|
+
Button6,
|
|
2862
|
+
{
|
|
2863
|
+
type: "button",
|
|
2864
|
+
variant: "ghost",
|
|
2865
|
+
size: "compact",
|
|
2866
|
+
disabled,
|
|
2867
|
+
onClick: onRemove,
|
|
2868
|
+
title: "Remove item",
|
|
2869
|
+
className: "text-muted-foreground hover:text-destructive h-8 w-8 p-0",
|
|
2870
|
+
children: /* @__PURE__ */ jsx26(Trash22, { className: "h-3.5 w-3.5" })
|
|
2871
|
+
}
|
|
2872
|
+
)
|
|
2873
|
+
]
|
|
2874
|
+
}
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/templates/index.ts
|
|
2879
|
+
var DEFAULT_TEMPLATES = {
|
|
2880
|
+
ArrayItemTemplate: DefaultArrayItemTemplate,
|
|
2881
|
+
HorizontalArrayItemTemplate,
|
|
2882
|
+
ArrayHeaderTemplate: DefaultArrayHeaderTemplate,
|
|
2883
|
+
ObjectContainerTemplate: DefaultObjectContainerTemplate
|
|
2884
|
+
};
|
|
2885
|
+
|
|
2886
|
+
// src/FormFieldRenderer.tsx
|
|
2887
|
+
import {
|
|
2888
|
+
getFieldTitle,
|
|
2889
|
+
getFieldHelpText,
|
|
2890
|
+
getFieldPlaceholder
|
|
2891
|
+
} from "@petrarca/sonnet-core/schema";
|
|
2892
|
+
import { jsx as jsx27 } from "react/jsx-runtime";
|
|
2893
|
+
function mergeFieldOptions(globalOptions, property, uiSchema) {
|
|
2894
|
+
const xWidgetOptions = property["x-ui-options"] || {};
|
|
2895
|
+
const fieldOptions = uiSchema?.["x-ui-options"] || {};
|
|
2896
|
+
return { ...globalOptions, ...xWidgetOptions, ...fieldOptions };
|
|
2897
|
+
}
|
|
2898
|
+
function resolveEffectiveLabel(label, uiTitle, globalLabelOption) {
|
|
2899
|
+
if (globalLabelOption === false) return "";
|
|
2900
|
+
if (typeof uiTitle === "string") return uiTitle;
|
|
2901
|
+
if (uiTitle === false) return "";
|
|
2902
|
+
return label;
|
|
2903
|
+
}
|
|
2904
|
+
function resolveFieldFlags(fieldOptions, globalOptions, disabled, readOnly) {
|
|
2905
|
+
return {
|
|
2906
|
+
disabled: fieldOptions.disabled ?? globalOptions.disabled ?? disabled,
|
|
2907
|
+
readOnly: fieldOptions.readonly ?? globalOptions.readonly ?? readOnly
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
function FormFieldRenderer({
|
|
2911
|
+
fieldName,
|
|
2912
|
+
property,
|
|
2913
|
+
value,
|
|
2914
|
+
onChange,
|
|
2915
|
+
onBlur,
|
|
2916
|
+
required,
|
|
2917
|
+
disabled,
|
|
2918
|
+
readOnly,
|
|
2919
|
+
schema,
|
|
2920
|
+
uiSchema,
|
|
2921
|
+
registry,
|
|
2922
|
+
globalOptions = {},
|
|
2923
|
+
formData
|
|
2924
|
+
}) {
|
|
2925
|
+
const label = getFieldTitle(fieldName, property);
|
|
2926
|
+
const description = uiSchema?.["x-ui-description"] ?? getFieldHelpText(property);
|
|
2927
|
+
const placeholder = uiSchema?.["x-ui-placeholder"] ?? getFieldPlaceholder(property);
|
|
2928
|
+
const Widget = resolveWidget(property, uiSchema, registry);
|
|
2929
|
+
const mergedOptions = mergeFieldOptions(globalOptions, property, uiSchema);
|
|
2930
|
+
if (mergedOptions.hidden) return null;
|
|
2931
|
+
const fieldOptions = uiSchema?.["x-ui-options"] || {};
|
|
2932
|
+
const flags = resolveFieldFlags(
|
|
2933
|
+
fieldOptions,
|
|
2934
|
+
globalOptions,
|
|
2935
|
+
disabled,
|
|
2936
|
+
readOnly
|
|
2937
|
+
);
|
|
2938
|
+
const effectiveLabel = resolveEffectiveLabel(
|
|
2939
|
+
label,
|
|
2940
|
+
uiSchema?.["x-ui-title"],
|
|
2941
|
+
globalOptions.label
|
|
2942
|
+
);
|
|
2943
|
+
return /* @__PURE__ */ jsx27(
|
|
2944
|
+
Widget,
|
|
2945
|
+
{
|
|
2946
|
+
id: `field-${fieldName}`,
|
|
2947
|
+
name: fieldName,
|
|
2948
|
+
value,
|
|
2949
|
+
onChange,
|
|
2950
|
+
onBlur,
|
|
2951
|
+
schema,
|
|
2952
|
+
propertySchema: property,
|
|
2953
|
+
uiSchema,
|
|
2954
|
+
registry,
|
|
2955
|
+
label: effectiveLabel,
|
|
2956
|
+
description,
|
|
2957
|
+
placeholder,
|
|
2958
|
+
required,
|
|
2959
|
+
disabled: flags.disabled,
|
|
2960
|
+
readonly: flags.readOnly,
|
|
2961
|
+
options: mergedOptions,
|
|
2962
|
+
formData
|
|
2963
|
+
}
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/hooks/useFormEngine.ts
|
|
2968
|
+
import { useState as useState7, useEffect as useEffect3, useMemo as useMemo6, useCallback as useCallback7, useRef as useRef5 } from "react";
|
|
2969
|
+
import { isEqual } from "@petrarca/sonnet-core";
|
|
2970
|
+
import {
|
|
2971
|
+
getSchemaProperties,
|
|
2972
|
+
getFieldDefault,
|
|
2973
|
+
applySchemaDefaults as applySchemaDefaults2,
|
|
2974
|
+
resolveSchema,
|
|
2975
|
+
resolveRefs,
|
|
2976
|
+
validateFormData,
|
|
2977
|
+
diffFormData
|
|
2978
|
+
} from "@petrarca/sonnet-core/schema";
|
|
2979
|
+
|
|
2980
|
+
// src/hooks/orderProperties.ts
|
|
2981
|
+
function orderProperties(properties, orderArray) {
|
|
2982
|
+
const allEntries = Object.entries(properties);
|
|
2983
|
+
if (!orderArray || orderArray.length === 0) return allEntries;
|
|
2984
|
+
const orderSet = new Set(orderArray);
|
|
2985
|
+
const map = new Map(allEntries);
|
|
2986
|
+
const ordered = [];
|
|
2987
|
+
for (const name of orderArray) {
|
|
2988
|
+
const prop = map.get(name);
|
|
2989
|
+
if (prop) ordered.push([name, prop]);
|
|
2990
|
+
}
|
|
2991
|
+
const unordered = allEntries.filter(([name]) => !orderSet.has(name));
|
|
2992
|
+
return [...ordered, ...unordered];
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// src/hooks/useFormEngine.ts
|
|
2996
|
+
function isImmediateField(fieldName, properties) {
|
|
2997
|
+
const property = properties[fieldName];
|
|
2998
|
+
return property?.type === "boolean" || property?.enum !== void 0;
|
|
2999
|
+
}
|
|
3000
|
+
function computeBaselineUpdate(newData, originalData, initialized, schemaPropertyNames) {
|
|
3001
|
+
const newCount = Object.keys(newData).length;
|
|
3002
|
+
const origCount = Object.keys(originalData).length;
|
|
3003
|
+
if (!initialized && newCount > 0) {
|
|
3004
|
+
const cleanedData = Object.fromEntries(
|
|
3005
|
+
Object.entries(newData).filter(([k]) => schemaPropertyNames.includes(k))
|
|
3006
|
+
);
|
|
3007
|
+
const nonSchema = Object.fromEntries(
|
|
3008
|
+
Object.entries(newData).filter(([k]) => !schemaPropertyNames.includes(k))
|
|
3009
|
+
);
|
|
3010
|
+
return { shouldMarkInitialized: true, cleanedData, nonSchema };
|
|
3011
|
+
}
|
|
3012
|
+
if (initialized && newCount > origCount) {
|
|
3013
|
+
const cleanedData = Object.fromEntries(
|
|
3014
|
+
Object.entries(newData).filter(([k]) => schemaPropertyNames.includes(k))
|
|
3015
|
+
);
|
|
3016
|
+
const nonSchema = Object.fromEntries(
|
|
3017
|
+
Object.entries(newData).filter(([k]) => !schemaPropertyNames.includes(k))
|
|
3018
|
+
);
|
|
3019
|
+
return { shouldMarkInitialized: false, cleanedData, nonSchema };
|
|
3020
|
+
}
|
|
3021
|
+
return null;
|
|
3022
|
+
}
|
|
3023
|
+
function useFormEngine({
|
|
3024
|
+
schema,
|
|
3025
|
+
data,
|
|
3026
|
+
uiSchema,
|
|
3027
|
+
onChange,
|
|
3028
|
+
onBlur,
|
|
3029
|
+
onValidationChange,
|
|
3030
|
+
onUpdate,
|
|
3031
|
+
onCancel,
|
|
3032
|
+
readOnly = false,
|
|
3033
|
+
showActions = false,
|
|
3034
|
+
deferChanges = false,
|
|
3035
|
+
deferStateUpdates = false
|
|
3036
|
+
}) {
|
|
3037
|
+
const refResolvedSchema = useMemo6(() => resolveRefs(schema), [schema]);
|
|
3038
|
+
const [originalData, setOriginalData] = useState7(
|
|
3039
|
+
() => applySchemaDefaults2(data, refResolvedSchema)
|
|
3040
|
+
);
|
|
3041
|
+
const [originalDataInitialized, setOriginalDataInitialized] = useState7(false);
|
|
3042
|
+
const [nonSchemaData, setNonSchemaData] = useState7(
|
|
3043
|
+
() => {
|
|
3044
|
+
const props = getSchemaProperties(refResolvedSchema);
|
|
3045
|
+
const names = Object.keys(props);
|
|
3046
|
+
return Object.fromEntries(
|
|
3047
|
+
Object.entries(data).filter(([k]) => !names.includes(k))
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
);
|
|
3051
|
+
const [formData, setFormData] = useState7(
|
|
3052
|
+
() => applySchemaDefaults2(data, refResolvedSchema)
|
|
3053
|
+
);
|
|
3054
|
+
const [pendingChanges, setPendingChanges] = useState7({});
|
|
3055
|
+
const resolvedSchema = useMemo6(
|
|
3056
|
+
() => resolveSchema(refResolvedSchema, formData),
|
|
3057
|
+
[refResolvedSchema, formData]
|
|
3058
|
+
);
|
|
3059
|
+
const orderedProperties = useMemo6(() => {
|
|
3060
|
+
const properties2 = getSchemaProperties(resolvedSchema);
|
|
3061
|
+
const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
|
|
3062
|
+
const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
|
|
3063
|
+
return orderProperties(properties2, orderArray);
|
|
3064
|
+
}, [resolvedSchema, uiSchema]);
|
|
3065
|
+
const [isValid, setIsValid] = useState7(() => {
|
|
3066
|
+
const initResolved = resolveSchema(refResolvedSchema, data);
|
|
3067
|
+
const validation = validateFormData(initResolved, data);
|
|
3068
|
+
return validation.isValid;
|
|
3069
|
+
});
|
|
3070
|
+
const [initialDefaultsSent, setInitialDefaultsSent] = useState7(false);
|
|
3071
|
+
const properties = getSchemaProperties(resolvedSchema);
|
|
3072
|
+
useEffect3(() => {
|
|
3073
|
+
if (!initialDefaultsSent) {
|
|
3074
|
+
const hasDefaults = Object.entries(properties).some(
|
|
3075
|
+
([fieldName, property]) => data[fieldName] === void 0 && getFieldDefault(property) !== void 0
|
|
3076
|
+
);
|
|
3077
|
+
if (hasDefaults && onChange) {
|
|
3078
|
+
onChange(formData);
|
|
3079
|
+
}
|
|
3080
|
+
setInitialDefaultsSent(true);
|
|
3081
|
+
}
|
|
3082
|
+
}, [initialDefaultsSent, properties, data, formData, onChange]);
|
|
3083
|
+
useEffect3(() => {
|
|
3084
|
+
const currentPropertyNames = Object.keys(properties);
|
|
3085
|
+
const keysToRemove = Object.keys(formData).filter(
|
|
3086
|
+
(k) => !currentPropertyNames.includes(k)
|
|
3087
|
+
);
|
|
3088
|
+
if (keysToRemove.length > 0) {
|
|
3089
|
+
const cleanedData = { ...formData };
|
|
3090
|
+
keysToRemove.forEach((k) => {
|
|
3091
|
+
delete cleanedData[k];
|
|
3092
|
+
});
|
|
3093
|
+
setFormData(cleanedData);
|
|
3094
|
+
if (onChange) onChange(cleanedData);
|
|
3095
|
+
}
|
|
3096
|
+
}, [properties, formData, onChange]);
|
|
3097
|
+
const prevDataRef = useRef5(data);
|
|
3098
|
+
useEffect3(() => {
|
|
3099
|
+
if (isEqual(prevDataRef.current, data)) return;
|
|
3100
|
+
prevDataRef.current = data;
|
|
3101
|
+
const newData = applySchemaDefaults2(data, refResolvedSchema);
|
|
3102
|
+
setFormData(newData);
|
|
3103
|
+
const schemaPropertyNames = Object.keys(properties);
|
|
3104
|
+
const result = computeBaselineUpdate(
|
|
3105
|
+
newData,
|
|
3106
|
+
originalData,
|
|
3107
|
+
originalDataInitialized,
|
|
3108
|
+
schemaPropertyNames
|
|
3109
|
+
);
|
|
3110
|
+
if (result) {
|
|
3111
|
+
setOriginalData(result.cleanedData);
|
|
3112
|
+
setNonSchemaData(result.nonSchema);
|
|
3113
|
+
if (result.shouldMarkInitialized) {
|
|
3114
|
+
setOriginalDataInitialized(true);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
const validation = validateFormData(resolvedSchema, newData);
|
|
3118
|
+
setIsValid(validation.isValid);
|
|
3119
|
+
}, [
|
|
3120
|
+
data,
|
|
3121
|
+
refResolvedSchema,
|
|
3122
|
+
resolvedSchema,
|
|
3123
|
+
properties,
|
|
3124
|
+
originalData,
|
|
3125
|
+
originalDataInitialized
|
|
3126
|
+
]);
|
|
3127
|
+
const hasChanges = useMemo6(
|
|
3128
|
+
() => !isEqual(formData, originalData),
|
|
3129
|
+
[formData, originalData]
|
|
3130
|
+
);
|
|
3131
|
+
const displayData = useMemo6(() => {
|
|
3132
|
+
if (deferStateUpdates && Object.keys(pendingChanges).length > 0) {
|
|
3133
|
+
return { ...formData, ...pendingChanges };
|
|
3134
|
+
}
|
|
3135
|
+
return formData;
|
|
3136
|
+
}, [formData, pendingChanges, deferStateUpdates]);
|
|
3137
|
+
const handleFieldChange = useCallback7(
|
|
3138
|
+
(fieldName, value) => {
|
|
3139
|
+
if (readOnly) return;
|
|
3140
|
+
const immediate = isImmediateField(fieldName, properties);
|
|
3141
|
+
if (showActions && deferStateUpdates && !immediate) {
|
|
3142
|
+
setPendingChanges((prev) => ({ ...prev, [fieldName]: value }));
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
const newData = { ...formData, [fieldName]: value };
|
|
3146
|
+
setFormData(newData);
|
|
3147
|
+
if (!showActions) {
|
|
3148
|
+
const shouldCallOnChange = !deferChanges || immediate;
|
|
3149
|
+
if (shouldCallOnChange && onChange) onChange(newData);
|
|
3150
|
+
}
|
|
3151
|
+
const validation = validateFormData(resolvedSchema, newData);
|
|
3152
|
+
setIsValid(validation.isValid);
|
|
3153
|
+
if (onValidationChange) {
|
|
3154
|
+
onValidationChange(validation.errors, validation.isValid);
|
|
3155
|
+
}
|
|
3156
|
+
},
|
|
3157
|
+
[
|
|
3158
|
+
readOnly,
|
|
3159
|
+
formData,
|
|
3160
|
+
deferChanges,
|
|
3161
|
+
deferStateUpdates,
|
|
3162
|
+
showActions,
|
|
3163
|
+
onChange,
|
|
3164
|
+
resolvedSchema,
|
|
3165
|
+
onValidationChange,
|
|
3166
|
+
properties
|
|
3167
|
+
]
|
|
3168
|
+
);
|
|
3169
|
+
const flushPendingField = useCallback7(
|
|
3170
|
+
(fieldName) => {
|
|
3171
|
+
if (!showActions || !deferStateUpdates) return;
|
|
3172
|
+
if (pendingChanges[fieldName] === void 0) return;
|
|
3173
|
+
const newData = { ...formData, ...pendingChanges };
|
|
3174
|
+
setFormData(newData);
|
|
3175
|
+
setPendingChanges((prev) => {
|
|
3176
|
+
const updated = { ...prev };
|
|
3177
|
+
delete updated[fieldName];
|
|
3178
|
+
return updated;
|
|
3179
|
+
});
|
|
3180
|
+
const validation = validateFormData(resolvedSchema, newData);
|
|
3181
|
+
setIsValid(validation.isValid);
|
|
3182
|
+
if (onValidationChange) {
|
|
3183
|
+
onValidationChange(validation.errors, validation.isValid);
|
|
3184
|
+
}
|
|
3185
|
+
},
|
|
3186
|
+
[
|
|
3187
|
+
showActions,
|
|
3188
|
+
deferStateUpdates,
|
|
3189
|
+
pendingChanges,
|
|
3190
|
+
formData,
|
|
3191
|
+
resolvedSchema,
|
|
3192
|
+
onValidationChange
|
|
3193
|
+
]
|
|
3194
|
+
);
|
|
3195
|
+
const handleFieldBlur = useCallback7(
|
|
3196
|
+
(fieldName) => {
|
|
3197
|
+
if (readOnly) return;
|
|
3198
|
+
flushPendingField(fieldName);
|
|
3199
|
+
if (!showActions && deferChanges && onChange) {
|
|
3200
|
+
onChange(formData);
|
|
3201
|
+
}
|
|
3202
|
+
if (onBlur) {
|
|
3203
|
+
const hasPending = deferStateUpdates && pendingChanges[fieldName] !== void 0;
|
|
3204
|
+
const dataToPass = hasPending ? { ...formData, [fieldName]: pendingChanges[fieldName] } : formData;
|
|
3205
|
+
onBlur(fieldName, dataToPass);
|
|
3206
|
+
}
|
|
3207
|
+
},
|
|
3208
|
+
[
|
|
3209
|
+
readOnly,
|
|
3210
|
+
formData,
|
|
3211
|
+
pendingChanges,
|
|
3212
|
+
deferChanges,
|
|
3213
|
+
deferStateUpdates,
|
|
3214
|
+
showActions,
|
|
3215
|
+
onChange,
|
|
3216
|
+
onBlur,
|
|
3217
|
+
flushPendingField
|
|
3218
|
+
]
|
|
3219
|
+
);
|
|
3220
|
+
const handleSave = useCallback7(() => {
|
|
3221
|
+
if (!hasChanges) return;
|
|
3222
|
+
const finalData = deferStateUpdates && Object.keys(pendingChanges).length > 0 ? { ...formData, ...pendingChanges } : formData;
|
|
3223
|
+
const completeData = { ...nonSchemaData, ...finalData };
|
|
3224
|
+
const validation = validateFormData(resolvedSchema, finalData);
|
|
3225
|
+
if (validation.isValid && onUpdate) {
|
|
3226
|
+
const changed = {};
|
|
3227
|
+
for (const key of Object.keys(finalData)) {
|
|
3228
|
+
if (!isEqual(finalData[key], originalData[key])) {
|
|
3229
|
+
changed[key] = finalData[key];
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
const diff = diffFormData(originalData, finalData);
|
|
3233
|
+
onUpdate(completeData, changed, diff);
|
|
3234
|
+
setPendingChanges({});
|
|
3235
|
+
}
|
|
3236
|
+
}, [
|
|
3237
|
+
hasChanges,
|
|
3238
|
+
resolvedSchema,
|
|
3239
|
+
formData,
|
|
3240
|
+
originalData,
|
|
3241
|
+
pendingChanges,
|
|
3242
|
+
deferStateUpdates,
|
|
3243
|
+
nonSchemaData,
|
|
3244
|
+
onUpdate
|
|
3245
|
+
]);
|
|
3246
|
+
const handleCancel = useCallback7(() => {
|
|
3247
|
+
setFormData({ ...originalData });
|
|
3248
|
+
setPendingChanges({});
|
|
3249
|
+
onCancel?.();
|
|
3250
|
+
}, [originalData, onCancel]);
|
|
3251
|
+
return {
|
|
3252
|
+
formData,
|
|
3253
|
+
displayData,
|
|
3254
|
+
resolvedSchema,
|
|
3255
|
+
orderedProperties,
|
|
3256
|
+
hasChanges,
|
|
3257
|
+
isValid,
|
|
3258
|
+
handleFieldChange,
|
|
3259
|
+
handleFieldBlur,
|
|
3260
|
+
handleSave,
|
|
3261
|
+
handleCancel
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// src/JsonSchemaFormRenderer.tsx
|
|
3266
|
+
import { jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3267
|
+
function stripFids(data) {
|
|
3268
|
+
const result = {};
|
|
3269
|
+
for (const [key, value] of Object.entries(data)) {
|
|
3270
|
+
if (Array.isArray(value)) {
|
|
3271
|
+
result[key] = value.map((item) => {
|
|
3272
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
3273
|
+
const rest = Object.fromEntries(
|
|
3274
|
+
Object.entries(item).filter(
|
|
3275
|
+
([k]) => k !== FORM_ITEM_ID_FIELD2
|
|
3276
|
+
)
|
|
3277
|
+
);
|
|
3278
|
+
return stripFids(rest);
|
|
3279
|
+
}
|
|
3280
|
+
return item;
|
|
3281
|
+
});
|
|
3282
|
+
} else if (value !== null && typeof value === "object") {
|
|
3283
|
+
result[key] = stripFids(value);
|
|
3284
|
+
} else {
|
|
3285
|
+
result[key] = value;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
return result;
|
|
3289
|
+
}
|
|
3290
|
+
function resolveFlagDefaults(props) {
|
|
3291
|
+
return {
|
|
3292
|
+
disabled: props.disabled ?? false,
|
|
3293
|
+
readOnly: props.readOnly ?? false,
|
|
3294
|
+
showActions: props.showActions ?? false,
|
|
3295
|
+
deferChanges: props.deferChanges ?? false,
|
|
3296
|
+
deferStateUpdates: props.deferStateUpdates ?? false,
|
|
3297
|
+
showCancel: props.showCancel ?? true,
|
|
3298
|
+
showExtraProperties: props.showExtraProperties ?? true,
|
|
3299
|
+
displayContents: props.displayContents ?? false
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
function resolveValueDefaults(props) {
|
|
3303
|
+
return {
|
|
3304
|
+
saveLabel: props.saveLabel ?? "Save",
|
|
3305
|
+
cancelLabel: props.cancelLabel ?? "Cancel",
|
|
3306
|
+
widgets: props.widgets ?? DEFAULT_WIDGETS,
|
|
3307
|
+
uiSchema: props.uiSchema ?? {}
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
function useWrappedCallbacks(onChange, onBlur, onUpdate) {
|
|
3311
|
+
const wrappedOnChange = useCallback8(
|
|
3312
|
+
(d) => onChange?.(stripFids(d)),
|
|
3313
|
+
[onChange]
|
|
3314
|
+
);
|
|
3315
|
+
const wrappedOnBlur = useCallback8(
|
|
3316
|
+
(fieldName, d) => onBlur?.(fieldName, stripFids(d)),
|
|
3317
|
+
[onBlur]
|
|
3318
|
+
);
|
|
3319
|
+
const wrappedOnUpdate = useCallback8(
|
|
3320
|
+
(d, changed, diff) => onUpdate?.(stripFids(d), stripFids(changed), diff),
|
|
3321
|
+
[onUpdate]
|
|
3322
|
+
);
|
|
3323
|
+
return {
|
|
3324
|
+
wrappedOnChange: onChange ? wrappedOnChange : void 0,
|
|
3325
|
+
wrappedOnBlur: onBlur ? wrappedOnBlur : void 0,
|
|
3326
|
+
wrappedOnUpdate: onUpdate ? wrappedOnUpdate : void 0
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
function buildFieldMap(fields) {
|
|
3330
|
+
const map = /* @__PURE__ */ new Map();
|
|
3331
|
+
for (const field of fields) {
|
|
3332
|
+
if (React11.isValidElement(field) && field.key != null) {
|
|
3333
|
+
map.set(String(field.key), field);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return map;
|
|
3337
|
+
}
|
|
3338
|
+
function buildRowElement(row, fieldMap, defaultRowGap) {
|
|
3339
|
+
const rowFields = row.fields.map((name) => fieldMap.get(name)).filter((el) => el != null);
|
|
3340
|
+
if (rowFields.length === 0) return null;
|
|
3341
|
+
const gridColumns = row.columns ?? `repeat(${rowFields.length}, 1fr)`;
|
|
3342
|
+
const rowGap = row.gap ?? defaultRowGap;
|
|
3343
|
+
return {
|
|
3344
|
+
element: /* @__PURE__ */ jsx28(
|
|
3345
|
+
"div",
|
|
3346
|
+
{
|
|
3347
|
+
style: {
|
|
3348
|
+
display: "grid",
|
|
3349
|
+
gridTemplateColumns: gridColumns,
|
|
3350
|
+
gap: `${rowGap}px`
|
|
3351
|
+
},
|
|
3352
|
+
children: rowFields
|
|
3353
|
+
},
|
|
3354
|
+
`row:${row.fields.join(",")}`
|
|
3355
|
+
),
|
|
3356
|
+
placed: row.fields
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
function collectUnplacedFields(fields, placed) {
|
|
3360
|
+
const result = [];
|
|
3361
|
+
for (const field of fields) {
|
|
3362
|
+
if (React11.isValidElement(field) && field.key != null && !placed.has(String(field.key))) {
|
|
3363
|
+
result.push(field);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return result;
|
|
3367
|
+
}
|
|
3368
|
+
function layoutFieldsInRows(fields, rowDefs, defaultRowGap) {
|
|
3369
|
+
const fieldMap = buildFieldMap(fields);
|
|
3370
|
+
const placed = /* @__PURE__ */ new Set();
|
|
3371
|
+
const elements = [];
|
|
3372
|
+
for (const row of rowDefs) {
|
|
3373
|
+
const result = buildRowElement(row, fieldMap, defaultRowGap);
|
|
3374
|
+
if (result == null) continue;
|
|
3375
|
+
elements.push(result.element);
|
|
3376
|
+
for (const name of result.placed) placed.add(name);
|
|
3377
|
+
}
|
|
3378
|
+
return elements.concat(collectUnplacedFields(fields, placed));
|
|
3379
|
+
}
|
|
3380
|
+
function FormContainer({
|
|
3381
|
+
displayContents,
|
|
3382
|
+
showExtraProperties,
|
|
3383
|
+
showActions,
|
|
3384
|
+
formData,
|
|
3385
|
+
resolvedSchema,
|
|
3386
|
+
handleSave,
|
|
3387
|
+
handleCancel,
|
|
3388
|
+
disabled,
|
|
3389
|
+
hasChanges,
|
|
3390
|
+
isValid,
|
|
3391
|
+
saveLabel,
|
|
3392
|
+
cancelLabel,
|
|
3393
|
+
showCancel,
|
|
3394
|
+
children
|
|
3395
|
+
}) {
|
|
3396
|
+
if (displayContents) {
|
|
3397
|
+
return /* @__PURE__ */ jsx28("div", { style: { display: "contents" }, children });
|
|
3398
|
+
}
|
|
3399
|
+
return /* @__PURE__ */ jsxs18(SimpleStack, { gap: 24, children: [
|
|
3400
|
+
children,
|
|
3401
|
+
showExtraProperties && /* @__PURE__ */ jsx28(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
|
|
3402
|
+
showActions && /* @__PURE__ */ jsx28(
|
|
3403
|
+
FormActions,
|
|
3404
|
+
{
|
|
3405
|
+
onSave: handleSave,
|
|
3406
|
+
onCancel: handleCancel,
|
|
3407
|
+
disabled,
|
|
3408
|
+
hasChanges,
|
|
3409
|
+
isValid,
|
|
3410
|
+
saveLabel,
|
|
3411
|
+
cancelLabel,
|
|
3412
|
+
showCancel
|
|
3413
|
+
}
|
|
3414
|
+
)
|
|
3415
|
+
] });
|
|
3416
|
+
}
|
|
3417
|
+
function JsonSchemaFormRenderer(props) {
|
|
3418
|
+
const {
|
|
3419
|
+
schema,
|
|
3420
|
+
data,
|
|
3421
|
+
onChange,
|
|
3422
|
+
onBlur,
|
|
3423
|
+
onValidationChange,
|
|
3424
|
+
onUpdate,
|
|
3425
|
+
onCancel,
|
|
3426
|
+
templates
|
|
3427
|
+
} = props;
|
|
3428
|
+
const {
|
|
3429
|
+
disabled,
|
|
3430
|
+
readOnly,
|
|
3431
|
+
showActions,
|
|
3432
|
+
deferChanges,
|
|
3433
|
+
deferStateUpdates,
|
|
3434
|
+
showCancel,
|
|
3435
|
+
showExtraProperties,
|
|
3436
|
+
displayContents
|
|
3437
|
+
} = resolveFlagDefaults(props);
|
|
3438
|
+
const { saveLabel, cancelLabel, widgets, uiSchema } = resolveValueDefaults(props);
|
|
3439
|
+
const { wrappedOnChange, wrappedOnBlur, wrappedOnUpdate } = useWrappedCallbacks(onChange, onBlur, onUpdate);
|
|
3440
|
+
const {
|
|
3441
|
+
formData,
|
|
3442
|
+
displayData,
|
|
3443
|
+
resolvedSchema,
|
|
3444
|
+
orderedProperties,
|
|
3445
|
+
hasChanges,
|
|
3446
|
+
isValid,
|
|
3447
|
+
handleFieldChange,
|
|
3448
|
+
handleFieldBlur,
|
|
3449
|
+
handleSave,
|
|
3450
|
+
handleCancel
|
|
3451
|
+
} = useFormEngine({
|
|
3452
|
+
schema,
|
|
3453
|
+
data,
|
|
3454
|
+
uiSchema,
|
|
3455
|
+
onChange: wrappedOnChange,
|
|
3456
|
+
onBlur: wrappedOnBlur,
|
|
3457
|
+
onValidationChange,
|
|
3458
|
+
onUpdate: wrappedOnUpdate,
|
|
3459
|
+
onCancel,
|
|
3460
|
+
readOnly,
|
|
3461
|
+
showActions,
|
|
3462
|
+
deferChanges,
|
|
3463
|
+
deferStateUpdates
|
|
3464
|
+
});
|
|
3465
|
+
const globalOptions = uiSchema["x-ui-globalOptions"] || {};
|
|
3466
|
+
const resolvedTemplates = useMemo7(
|
|
3467
|
+
() => ({ ...DEFAULT_TEMPLATES, ...templates }),
|
|
3468
|
+
[templates]
|
|
3469
|
+
);
|
|
3470
|
+
const nestedRenderer = useCallback8(
|
|
3471
|
+
(props2) => /* @__PURE__ */ jsx28(
|
|
3472
|
+
JsonSchemaFormRenderer,
|
|
3473
|
+
{
|
|
3474
|
+
...props2,
|
|
3475
|
+
templates: resolvedTemplates,
|
|
3476
|
+
showActions: false,
|
|
3477
|
+
showExtraProperties: false
|
|
3478
|
+
}
|
|
3479
|
+
),
|
|
3480
|
+
[resolvedTemplates]
|
|
3481
|
+
);
|
|
3482
|
+
const contextValue = useMemo7(
|
|
3483
|
+
() => ({ renderer: nestedRenderer, templates: resolvedTemplates }),
|
|
3484
|
+
[nestedRenderer, resolvedTemplates]
|
|
3485
|
+
);
|
|
3486
|
+
const needsBlurHandler = deferStateUpdates || !!onBlur;
|
|
3487
|
+
const fields = orderedProperties.map(([fieldName, property]) => /* @__PURE__ */ jsx28(
|
|
3488
|
+
FormFieldRenderer,
|
|
3489
|
+
{
|
|
3490
|
+
fieldName,
|
|
3491
|
+
property,
|
|
3492
|
+
value: displayData[fieldName],
|
|
3493
|
+
onChange: (v) => handleFieldChange(fieldName, v),
|
|
3494
|
+
onBlur: needsBlurHandler ? () => handleFieldBlur(fieldName) : void 0,
|
|
3495
|
+
required: isFieldRequired(resolvedSchema, fieldName),
|
|
3496
|
+
disabled,
|
|
3497
|
+
readOnly,
|
|
3498
|
+
schema,
|
|
3499
|
+
uiSchema: uiSchema[fieldName],
|
|
3500
|
+
registry: widgets,
|
|
3501
|
+
globalOptions,
|
|
3502
|
+
formData: displayData
|
|
3503
|
+
},
|
|
3504
|
+
fieldName
|
|
3505
|
+
));
|
|
3506
|
+
const layoutConfig = uiSchema["x-ui-layout"];
|
|
3507
|
+
const rowDefs = layoutConfig?.rows;
|
|
3508
|
+
const defaultRowGap = layoutConfig?.gap ?? 24;
|
|
3509
|
+
const layoutFields = useMemo7(
|
|
3510
|
+
() => rowDefs && rowDefs.length > 0 ? layoutFieldsInRows(fields, rowDefs, defaultRowGap) : fields,
|
|
3511
|
+
[fields, rowDefs, defaultRowGap]
|
|
3512
|
+
);
|
|
3513
|
+
return /* @__PURE__ */ jsx28(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx28(
|
|
3514
|
+
FormContainer,
|
|
3515
|
+
{
|
|
3516
|
+
displayContents,
|
|
3517
|
+
showExtraProperties,
|
|
3518
|
+
showActions,
|
|
3519
|
+
formData,
|
|
3520
|
+
resolvedSchema,
|
|
3521
|
+
handleSave,
|
|
3522
|
+
handleCancel,
|
|
3523
|
+
disabled,
|
|
3524
|
+
hasChanges,
|
|
3525
|
+
isValid,
|
|
3526
|
+
saveLabel,
|
|
3527
|
+
cancelLabel,
|
|
3528
|
+
showCancel,
|
|
3529
|
+
children: layoutFields
|
|
3530
|
+
}
|
|
3531
|
+
) });
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// src/hooks/useResolvedSchema.ts
|
|
3535
|
+
import { useMemo as useMemo8 } from "react";
|
|
3536
|
+
import {
|
|
3537
|
+
getSchemaProperties as getSchemaProperties2,
|
|
3538
|
+
resolveSchema as resolveSchema2
|
|
3539
|
+
} from "@petrarca/sonnet-core/schema";
|
|
3540
|
+
function useResolvedSchema({
|
|
3541
|
+
schema,
|
|
3542
|
+
formData,
|
|
3543
|
+
uiSchema
|
|
3544
|
+
}) {
|
|
3545
|
+
const resolvedSchema = useMemo8(
|
|
3546
|
+
() => resolveSchema2(schema, formData),
|
|
3547
|
+
[schema, formData]
|
|
3548
|
+
);
|
|
3549
|
+
const orderedProperties = useMemo8(() => {
|
|
3550
|
+
const properties = getSchemaProperties2(resolvedSchema);
|
|
3551
|
+
const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
|
|
3552
|
+
const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
|
|
3553
|
+
return orderProperties(properties, orderArray);
|
|
3554
|
+
}, [resolvedSchema, uiSchema]);
|
|
3555
|
+
return { resolvedSchema, orderedProperties };
|
|
3556
|
+
}
|
|
3557
|
+
export {
|
|
3558
|
+
ArrayWidget,
|
|
3559
|
+
CheckboxWidget,
|
|
3560
|
+
DEFAULT_TEMPLATES,
|
|
3561
|
+
DEFAULT_WIDGETS,
|
|
3562
|
+
DEFAULT_WIDGET_MAP,
|
|
3563
|
+
EntitySelectWidget,
|
|
3564
|
+
ExtraPropertiesCard_default as ExtraPropertiesCard,
|
|
3565
|
+
FormActions,
|
|
3566
|
+
FormCheckbox,
|
|
3567
|
+
FormFieldRenderer,
|
|
3568
|
+
FormFieldWrapper,
|
|
3569
|
+
FormInput,
|
|
3570
|
+
FormMultiSelect,
|
|
3571
|
+
FormNumberInput,
|
|
3572
|
+
FormQuantityInput,
|
|
3573
|
+
FormSelect,
|
|
3574
|
+
FormTagsInput,
|
|
3575
|
+
FormTextarea,
|
|
3576
|
+
JsonEditorWidget,
|
|
3577
|
+
JsonSchemaFormRenderer,
|
|
3578
|
+
NumberWidget,
|
|
3579
|
+
ObjectWidget,
|
|
3580
|
+
QuantityWidget,
|
|
3581
|
+
SelectWidget,
|
|
3582
|
+
TagsInputWidget,
|
|
3583
|
+
TextInputWidget,
|
|
3584
|
+
TextareaWidget,
|
|
3585
|
+
firstError,
|
|
3586
|
+
getAriaDescribedBy,
|
|
3587
|
+
getDefaultWidgetForType,
|
|
3588
|
+
resolveWidget,
|
|
3589
|
+
useFormEngine,
|
|
3590
|
+
useNestedFormContext,
|
|
3591
|
+
useResolvedSchema
|
|
3592
|
+
};
|
|
3593
|
+
//# sourceMappingURL=index.js.map
|