@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/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