@petrarca/sonnet-forms 0.4.2 → 0.6.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 CHANGED
@@ -8,39 +8,39 @@ function FormFieldWrapper({
8
8
  error,
9
9
  required,
10
10
  compact,
11
+ labelPlacement = "top",
11
12
  className,
12
13
  children
13
14
  }) {
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
- ),
15
+ if (compact) return /* @__PURE__ */ jsx("div", { className, children });
16
+ const labelEl = !!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
+ const descriptionEl = description && /* @__PURE__ */ jsx("p", { id: `${inputId}-description`, className: "text-sm text-muted-foreground", children: description });
28
+ const errorEl = error && /* @__PURE__ */ jsx("p", { id: `${inputId}-error`, className: "text-sm font-medium text-destructive", children: error });
29
+ if (labelPlacement === "start" && !!label) {
30
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", className), children: [
31
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 whitespace-nowrap", children: labelEl }),
32
+ /* @__PURE__ */ jsxs("div", { className: "w-full min-w-0 space-y-1", children: [
33
+ descriptionEl,
34
+ children,
35
+ errorEl
36
+ ] })
37
+ ] });
38
+ }
39
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
40
+ labelEl,
41
+ descriptionEl,
35
42
  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
- )
43
+ errorEl
44
44
  ] });
45
45
  }
46
46
 
@@ -84,6 +84,7 @@ var FormInput = React.forwardRef(
84
84
  rightSection,
85
85
  rightSectionWidth,
86
86
  compact,
87
+ labelPlacement,
87
88
  ...props
88
89
  }, ref) => {
89
90
  const generatedId = useId();
@@ -98,6 +99,7 @@ var FormInput = React.forwardRef(
98
99
  error,
99
100
  required,
100
101
  compact,
102
+ labelPlacement,
101
103
  className: wrapperClassName,
102
104
  children: /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
103
105
  /* @__PURE__ */ jsx2(
@@ -170,16 +172,21 @@ var FormTextarea = React2.forwardRef((allProps, ref) => {
170
172
  error,
171
173
  required,
172
174
  wrapperClassName,
175
+ labelPlacement,
173
176
  className,
174
177
  id,
175
178
  value,
176
179
  onChange,
177
180
  rightSection,
178
181
  rightSectionWidth,
182
+ // Destructured out so they are not spread onto the DOM textarea below;
183
+ // these are component-level props, not valid HTML attributes.
184
+ autosize: autosizeProp,
185
+ minRows: minRowsProp,
179
186
  ...props
180
187
  } = allProps;
181
- const autosize = allProps.autosize ?? false;
182
- const minRows = allProps.minRows ?? 3;
188
+ const autosize = autosizeProp ?? false;
189
+ const minRows = minRowsProp ?? 3;
183
190
  const generatedId = useId2();
184
191
  const textareaId = id ?? generatedId;
185
192
  const internalRef = useRef(null);
@@ -196,6 +203,7 @@ var FormTextarea = React2.forwardRef((allProps, ref) => {
196
203
  description,
197
204
  error,
198
205
  required,
206
+ labelPlacement,
199
207
  className: cn3("flex flex-col", wrapperClassName),
200
208
  children: /* @__PURE__ */ jsxs3("div", { className: "relative flex-1 min-h-0", children: [
201
209
  /* @__PURE__ */ jsx3(
@@ -246,7 +254,8 @@ var FormNumberInput = React3.forwardRef(
246
254
  wrapperClassName,
247
255
  className,
248
256
  id,
249
- name
257
+ name,
258
+ labelPlacement
250
259
  }, ref) => {
251
260
  const generatedId = useId3();
252
261
  const inputId = id ?? generatedId;
@@ -269,6 +278,7 @@ var FormNumberInput = React3.forwardRef(
269
278
  description,
270
279
  error,
271
280
  required,
281
+ labelPlacement,
272
282
  className: wrapperClassName,
273
283
  children: /* @__PURE__ */ jsx4(
274
284
  Input2,
@@ -422,7 +432,7 @@ FormCheckbox.displayName = "FormCheckbox";
422
432
 
423
433
  // src/FormSelect.tsx
424
434
  import * as React5 from "react";
425
- import { Check, ChevronsUpDown, X } from "lucide-react";
435
+ import { Check, ChevronsUpDown, Plus, X } from "lucide-react";
426
436
  import { cn as cn6 } from "@petrarca/sonnet-core";
427
437
  import { Button } from "@petrarca/sonnet-ui";
428
438
  import {
@@ -434,12 +444,28 @@ import {
434
444
  CommandList
435
445
  } from "@petrarca/sonnet-ui";
436
446
  import { Popover, PopoverContent, PopoverTrigger } from "@petrarca/sonnet-ui";
447
+
448
+ // src/formSelectUtils.ts
449
+ function canCreateFromSearch(allowCreate, searchQuery, options) {
450
+ const trimmed = searchQuery.trim();
451
+ if (!allowCreate || trimmed.length === 0) return false;
452
+ const lower = trimmed.toLowerCase();
453
+ return !options.some((o) => o.label.toLowerCase() === lower);
454
+ }
455
+
456
+ // src/FormSelect.tsx
437
457
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
438
- function useSelectState(value, onChange, onBlur) {
458
+ function useSelectState(value, onChange, onBlur, onCreate, clearable) {
439
459
  const [open, setOpen] = React5.useState(false);
440
460
  const [searchQuery, setSearchQuery] = React5.useState("");
441
461
  const handleSelect = (selectedValue) => {
442
- onChange?.(selectedValue === value ? null : selectedValue);
462
+ const next = clearable && selectedValue === value ? null : selectedValue;
463
+ onChange?.(next);
464
+ setOpen(false);
465
+ setSearchQuery("");
466
+ };
467
+ const handleCreate = (label) => {
468
+ onCreate?.(label);
443
469
  setOpen(false);
444
470
  setSearchQuery("");
445
471
  };
@@ -458,6 +484,7 @@ function useSelectState(value, onChange, onBlur) {
458
484
  searchQuery,
459
485
  setSearchQuery,
460
486
  handleSelect,
487
+ handleCreate,
461
488
  handleClear,
462
489
  handleOpenChange
463
490
  };
@@ -479,8 +506,11 @@ function SelectDropdown({
479
506
  emptyMessage,
480
507
  options,
481
508
  value,
482
- onSelect
509
+ onSelect,
510
+ allowCreate = false,
511
+ onCreate
483
512
  }) {
513
+ const showCreate = canCreateFromSearch(allowCreate, searchQuery, options);
484
514
  return /* @__PURE__ */ jsxs5(Command, { shouldFilter: false, children: [
485
515
  /* @__PURE__ */ jsx6(
486
516
  CommandInput,
@@ -491,29 +521,45 @@ function SelectDropdown({
491
521
  }
492
522
  ),
493
523
  /* @__PURE__ */ jsxs5(CommandList, { children: [
494
- /* @__PURE__ */ jsx6(CommandEmpty, { children: loading ? "Loading..." : emptyMessage }),
495
- /* @__PURE__ */ jsx6(CommandGroup, { children: options.filter(
496
- (option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())
497
- ).map((option) => /* @__PURE__ */ jsxs5(
498
- CommandItem,
499
- {
500
- value: option.value,
501
- onSelect,
502
- children: [
503
- /* @__PURE__ */ jsx6(
504
- Check,
505
- {
506
- className: cn6(
507
- "mr-2 h-4 w-4",
508
- value === option.value ? "opacity-100" : "opacity-0"
509
- )
510
- }
511
- ),
512
- option.label
513
- ]
514
- },
515
- option.value
516
- )) })
524
+ !showCreate && /* @__PURE__ */ jsx6(CommandEmpty, { children: loading ? "Loading..." : emptyMessage }),
525
+ /* @__PURE__ */ jsxs5(CommandGroup, { children: [
526
+ options.filter(
527
+ (option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())
528
+ ).map((option) => /* @__PURE__ */ jsxs5(
529
+ CommandItem,
530
+ {
531
+ value: option.value,
532
+ onSelect,
533
+ children: [
534
+ /* @__PURE__ */ jsx6(
535
+ Check,
536
+ {
537
+ className: cn6(
538
+ "mr-2 h-4 w-4",
539
+ value === option.value ? "opacity-100" : "opacity-0"
540
+ )
541
+ }
542
+ ),
543
+ option.label
544
+ ]
545
+ },
546
+ option.value
547
+ )),
548
+ showCreate && /* @__PURE__ */ jsxs5(
549
+ CommandItem,
550
+ {
551
+ value: `__create__${searchQuery}`,
552
+ onSelect: () => onCreate?.(searchQuery.trim()),
553
+ children: [
554
+ /* @__PURE__ */ jsx6(Plus, { className: "mr-2 h-4 w-4" }),
555
+ "Create \u201C",
556
+ searchQuery.trim(),
557
+ "\u201D"
558
+ ]
559
+ },
560
+ "__create__"
561
+ )
562
+ ] })
517
563
  ] })
518
564
  ] });
519
565
  }
@@ -536,13 +582,16 @@ function FormSelect(props) {
536
582
  required,
537
583
  wrapperClassName,
538
584
  compact,
585
+ labelPlacement,
539
586
  options,
540
587
  value,
541
588
  onChange,
542
589
  onBlur,
543
590
  className,
544
591
  id,
545
- autoFocus
592
+ autoFocus,
593
+ allowCreate,
594
+ onCreate
546
595
  } = props;
547
596
  const {
548
597
  placeholder,
@@ -560,9 +609,10 @@ function FormSelect(props) {
560
609
  searchQuery,
561
610
  setSearchQuery,
562
611
  handleSelect,
612
+ handleCreate,
563
613
  handleClear,
564
614
  handleOpenChange
565
- } = useSelectState(value, onChange, onBlur);
615
+ } = useSelectState(value, onChange, onBlur, onCreate, clearable);
566
616
  const selectedOption = options.find((opt) => opt.value === value);
567
617
  const isDisabled = disabled || readOnly || loading;
568
618
  return /* @__PURE__ */ jsx6(
@@ -574,6 +624,7 @@ function FormSelect(props) {
574
624
  error,
575
625
  required,
576
626
  compact,
627
+ labelPlacement,
577
628
  className: wrapperClassName,
578
629
  children: /* @__PURE__ */ jsxs5(Popover, { open, onOpenChange: handleOpenChange, children: [
579
630
  /* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs5(
@@ -627,7 +678,9 @@ function FormSelect(props) {
627
678
  emptyMessage,
628
679
  options,
629
680
  value,
630
- onSelect: handleSelect
681
+ onSelect: handleSelect,
682
+ allowCreate,
683
+ onCreate: handleCreate
631
684
  }
632
685
  )
633
686
  }
@@ -661,13 +714,22 @@ function RemovableBadge({
661
714
  }) {
662
715
  return /* @__PURE__ */ jsxs6(Badge, { variant: "secondary", badgeColor, className: "mr-1", children: [
663
716
  option.label,
664
- !isDisabled && /* @__PURE__ */ jsx7(
665
- "button",
717
+ !isDisabled && // Rendered as a role="button" span, not a <button>: this badge lives
718
+ // inside the combobox trigger <button>, and a button cannot be nested
719
+ // in a button (invalid HTML / hydration error). The span keeps click +
720
+ // Enter/Space activation and a focus ring.
721
+ /* @__PURE__ */ jsx7(
722
+ "span",
666
723
  {
667
- type: "button",
668
- className: "ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
724
+ role: "button",
725
+ tabIndex: 0,
726
+ "aria-label": `Remove ${option.label}`,
727
+ className: "ml-1 inline-flex cursor-pointer ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
669
728
  onKeyDown: (e) => {
670
- if (e.key === "Enter") onRemove(option.value, e);
729
+ if (e.key === "Enter" || e.key === " ") {
730
+ e.preventDefault();
731
+ onRemove(option.value, e);
732
+ }
671
733
  },
672
734
  onMouseDown: (e) => {
673
735
  e.preventDefault();
@@ -852,6 +914,7 @@ function FormMultiSelect(props) {
852
914
  error,
853
915
  required,
854
916
  wrapperClassName,
917
+ labelPlacement,
855
918
  options,
856
919
  onChange,
857
920
  onBlur,
@@ -897,6 +960,7 @@ function FormMultiSelect(props) {
897
960
  description,
898
961
  error,
899
962
  required,
963
+ labelPlacement,
900
964
  className: wrapperClassName,
901
965
  children: [
902
966
  /* @__PURE__ */ jsxs6(Popover2, { open, onOpenChange: handleOpenChange, children: [
@@ -1063,6 +1127,7 @@ var FormTagsInput = React7.forwardRef((props, ref) => {
1063
1127
  description,
1064
1128
  error,
1065
1129
  wrapperClassName,
1130
+ labelPlacement,
1066
1131
  id,
1067
1132
  name,
1068
1133
  value = [],
@@ -1089,6 +1154,7 @@ var FormTagsInput = React7.forwardRef((props, ref) => {
1089
1154
  description,
1090
1155
  error,
1091
1156
  required,
1157
+ labelPlacement,
1092
1158
  className: wrapperClassName,
1093
1159
  children: /* @__PURE__ */ jsxs7(
1094
1160
  "div",
@@ -1292,6 +1358,7 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1292
1358
  readOnly,
1293
1359
  autoFocus,
1294
1360
  wrapperClassName,
1361
+ labelPlacement,
1295
1362
  className,
1296
1363
  name
1297
1364
  } = allProps;
@@ -1361,6 +1428,7 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1361
1428
  description,
1362
1429
  error: resolvedError,
1363
1430
  required,
1431
+ labelPlacement,
1364
1432
  className: wrapperClassName,
1365
1433
  children: /* @__PURE__ */ jsxs8(
1366
1434
  InputGroup,
@@ -1465,9 +1533,13 @@ function ExtraPropertiesCard({ properties, schema }) {
1465
1533
  var ExtraPropertiesCard_default = ExtraPropertiesCard;
1466
1534
 
1467
1535
  // src/JsonSchemaFormRenderer.tsx
1468
- import React11, { useCallback as useCallback8, useMemo as useMemo7 } from "react";
1536
+ import React13, { useCallback as useCallback8, useMemo as useMemo8 } from "react";
1469
1537
  import { SimpleStack } from "@petrarca/sonnet-ui";
1470
- import { isFieldRequired } from "@petrarca/sonnet-core/schema";
1538
+ import { AlertCircle as AlertCircle2 } from "lucide-react";
1539
+ import {
1540
+ WIDTH_TOKEN_REMS
1541
+ } from "@petrarca/sonnet-core/schema";
1542
+ import { isFieldRequired, evaluateUiRule } from "@petrarca/sonnet-core/schema";
1471
1543
  import { FORM_ITEM_ID_FIELD as FORM_ITEM_ID_FIELD2 } from "@petrarca/sonnet-core";
1472
1544
 
1473
1545
  // src/widgets/types.ts
@@ -1510,6 +1582,7 @@ function TextInputWidget({
1510
1582
  onChange: (event) => onChange(event.currentTarget.value),
1511
1583
  onBlur: () => onBlur?.(id, value),
1512
1584
  label,
1585
+ labelPlacement: options?.labelPlacement,
1513
1586
  description,
1514
1587
  placeholder,
1515
1588
  required,
@@ -1557,6 +1630,7 @@ function TextareaWidget({
1557
1630
  onChange: (event) => onChange(event.currentTarget.value),
1558
1631
  onBlur: () => onBlur?.(id, value),
1559
1632
  label,
1633
+ labelPlacement: options?.labelPlacement,
1560
1634
  description,
1561
1635
  placeholder,
1562
1636
  required,
@@ -1604,6 +1678,7 @@ function NumberWidget({
1604
1678
  onChange: (val) => onChange(val),
1605
1679
  onBlur: () => onBlur?.(id, value),
1606
1680
  label,
1681
+ labelPlacement: options.labelPlacement,
1607
1682
  description,
1608
1683
  placeholder,
1609
1684
  required,
@@ -1720,6 +1795,7 @@ function SelectWidget(props) {
1720
1795
  const enumNames = widgetOptions.enumNames || {};
1721
1796
  const clearable = widgetOptions.clearable !== false && !required;
1722
1797
  const isMultiple = widgetOptions.multiple ?? propertySchema.type === "array";
1798
+ const creatable = !!widgetOptions.creatable;
1723
1799
  const selectOptions = buildSelectOptions(propertySchema, enumNames);
1724
1800
  const error = firstError(rawErrors);
1725
1801
  const compact = widgetOptions?.compact;
@@ -1735,6 +1811,7 @@ function SelectWidget(props) {
1735
1811
  value: normalizeMultiValue(value),
1736
1812
  onChange: (newValue) => onChange(newValue || []),
1737
1813
  label,
1814
+ labelPlacement: widgetOptions.labelPlacement,
1738
1815
  description,
1739
1816
  placeholder,
1740
1817
  required,
@@ -1758,6 +1835,7 @@ function SelectWidget(props) {
1758
1835
  value: normalizeSingleValue(value),
1759
1836
  onChange: (val) => onChange(val || ""),
1760
1837
  label,
1838
+ labelPlacement: widgetOptions.labelPlacement,
1761
1839
  description,
1762
1840
  placeholder,
1763
1841
  required,
@@ -1768,7 +1846,9 @@ function SelectWidget(props) {
1768
1846
  autoFocus: autofocus,
1769
1847
  onBlur: handleBlur,
1770
1848
  className: triggerClassName,
1771
- compact
1849
+ compact,
1850
+ allowCreate: creatable,
1851
+ onCreate: (label2) => onChange(label2)
1772
1852
  }
1773
1853
  );
1774
1854
  }
@@ -1799,6 +1879,7 @@ function TagsInputWidget({
1799
1879
  value: tags,
1800
1880
  onChange: (newTags) => onChange(newTags),
1801
1881
  label,
1882
+ labelPlacement: options?.labelPlacement,
1802
1883
  description,
1803
1884
  placeholder: placeholder ?? "Type and press Enter",
1804
1885
  required,
@@ -1880,11 +1961,14 @@ function resolveMinChars(options) {
1880
1961
  }
1881
1962
  function buildRestConfig(options) {
1882
1963
  if (!("endpoint" in options) || !options.endpoint) return null;
1964
+ const rawExtra = options.extraParams;
1965
+ const extraParams = rawExtra && typeof rawExtra === "object" ? rawExtra : void 0;
1883
1966
  return {
1884
1967
  endpoint: String(options.endpoint),
1885
1968
  valueKey: String(options.valueKey ?? "id"),
1886
1969
  labelKey: String(options.labelKey ?? "name"),
1887
- searchKey: options.searchKey ? String(options.searchKey) : void 0
1970
+ searchKey: options.searchKey ? String(options.searchKey) : void 0,
1971
+ extraParams
1888
1972
  };
1889
1973
  }
1890
1974
  function buildGraphqlConfig(options) {
@@ -1948,12 +2032,13 @@ function EntitySelectWidget({
1948
2032
  mode: selectMode,
1949
2033
  minChars: selectMinChars,
1950
2034
  value: typeof value === "string" ? value : null,
1951
- onChange: (v) => onChange(v ?? ""),
2035
+ onChange: (v) => onChange(v ?? null),
1952
2036
  onBlur: () => onBlur?.(id, value),
1953
2037
  label: label || false,
1954
2038
  description,
1955
2039
  placeholder,
1956
2040
  required,
2041
+ clearable: !required,
1957
2042
  disabled: disabled || readonly,
1958
2043
  error: firstError(rawErrors)
1959
2044
  }
@@ -2166,7 +2251,7 @@ function ObjectWidget({
2166
2251
 
2167
2252
  // src/widgets/ArrayWidget.tsx
2168
2253
  import { useCallback as useCallback5, useMemo as useMemo5 } from "react";
2169
- import { Plus } from "lucide-react";
2254
+ import { Plus as Plus2 } from "lucide-react";
2170
2255
  import { Button as Button5 } from "@petrarca/sonnet-ui";
2171
2256
  import { applySchemaDefaults } from "@petrarca/sonnet-core/schema";
2172
2257
  import { FORM_ITEM_ID_FIELD, generateId } from "@petrarca/sonnet-core";
@@ -2182,53 +2267,61 @@ function DefaultArrayItemTemplate({
2182
2267
  canRemove,
2183
2268
  orderable,
2184
2269
  disabled,
2270
+ fid,
2185
2271
  onRemove,
2186
2272
  onMoveUp,
2187
2273
  onMoveDown
2188
2274
  }) {
2189
2275
  const showControls = canRemove || orderable;
2190
- return /* @__PURE__ */ jsxs14("div", { className: "relative flex gap-2 rounded-md border border-border/50 bg-muted/20 p-3", children: [
2191
- /* @__PURE__ */ jsx22("div", { className: "flex-1 min-w-0", children }),
2192
- showControls && /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1 pt-0.5", children: [
2193
- orderable && index > 0 && /* @__PURE__ */ jsx22(
2194
- Button4,
2195
- {
2196
- type: "button",
2197
- variant: "ghost",
2198
- size: "compact",
2199
- disabled,
2200
- onClick: onMoveUp,
2201
- title: "Move up",
2202
- children: /* @__PURE__ */ jsx22(ChevronUp, { className: "h-3.5 w-3.5" })
2203
- }
2204
- ),
2205
- orderable && index < total - 1 && /* @__PURE__ */ jsx22(
2206
- Button4,
2207
- {
2208
- type: "button",
2209
- variant: "ghost",
2210
- size: "compact",
2211
- disabled,
2212
- onClick: onMoveDown,
2213
- title: "Move down",
2214
- children: /* @__PURE__ */ jsx22(ChevronDown3, { className: "h-3.5 w-3.5" })
2215
- }
2216
- ),
2217
- canRemove && /* @__PURE__ */ jsx22(
2218
- Button4,
2219
- {
2220
- type: "button",
2221
- variant: "ghost",
2222
- size: "compact",
2223
- disabled,
2224
- onClick: onRemove,
2225
- title: "Remove item",
2226
- className: "text-muted-foreground hover:text-destructive",
2227
- children: /* @__PURE__ */ jsx22(Trash2, { className: "h-3.5 w-3.5" })
2228
- }
2229
- )
2230
- ] })
2231
- ] });
2276
+ return /* @__PURE__ */ jsxs14(
2277
+ "div",
2278
+ {
2279
+ "data-fid": fid,
2280
+ className: "relative flex gap-2 rounded-md border border-border/50 bg-muted/20 p-3",
2281
+ children: [
2282
+ /* @__PURE__ */ jsx22("div", { className: "flex-1 min-w-0", children }),
2283
+ showControls && /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1 pt-0.5", children: [
2284
+ orderable && index > 0 && /* @__PURE__ */ jsx22(
2285
+ Button4,
2286
+ {
2287
+ type: "button",
2288
+ variant: "ghost",
2289
+ size: "compact",
2290
+ disabled,
2291
+ onClick: onMoveUp,
2292
+ title: "Move up",
2293
+ children: /* @__PURE__ */ jsx22(ChevronUp, { className: "h-3.5 w-3.5" })
2294
+ }
2295
+ ),
2296
+ orderable && index < total - 1 && /* @__PURE__ */ jsx22(
2297
+ Button4,
2298
+ {
2299
+ type: "button",
2300
+ variant: "ghost",
2301
+ size: "compact",
2302
+ disabled,
2303
+ onClick: onMoveDown,
2304
+ title: "Move down",
2305
+ children: /* @__PURE__ */ jsx22(ChevronDown3, { className: "h-3.5 w-3.5" })
2306
+ }
2307
+ ),
2308
+ canRemove && /* @__PURE__ */ jsx22(
2309
+ Button4,
2310
+ {
2311
+ type: "button",
2312
+ variant: "ghost",
2313
+ size: "compact",
2314
+ disabled,
2315
+ onClick: onRemove,
2316
+ title: "Remove item",
2317
+ className: "text-muted-foreground hover:text-destructive",
2318
+ children: /* @__PURE__ */ jsx22(Trash2, { className: "h-3.5 w-3.5" })
2319
+ }
2320
+ )
2321
+ ] })
2322
+ ]
2323
+ }
2324
+ );
2232
2325
  }
2233
2326
 
2234
2327
  // src/templates/gridUtils.ts
@@ -2494,6 +2587,7 @@ function ArrayWidgetInner({
2494
2587
  disabled,
2495
2588
  layout,
2496
2589
  fieldCount,
2590
+ fid: item[FORM_ITEM_ID_FIELD],
2497
2591
  onRemove: () => onRemove(index),
2498
2592
  onMoveUp: index > 0 ? () => onMove(index, index - 1) : void 0,
2499
2593
  onMoveDown: index < items.length - 1 ? () => onMove(index, index + 1) : void 0,
@@ -2528,7 +2622,7 @@ function ArrayWidgetInner({
2528
2622
  disabled,
2529
2623
  className: "gap-1.5",
2530
2624
  children: [
2531
- /* @__PURE__ */ jsx24(Plus, { className: "h-3.5 w-3.5" }),
2625
+ /* @__PURE__ */ jsx24(Plus2, { className: "h-3.5 w-3.5" }),
2532
2626
  display.addLabel
2533
2627
  ]
2534
2628
  }
@@ -2542,6 +2636,13 @@ import { jsx as jsx25 } from "react/jsx-runtime";
2542
2636
  function isUnitOption(u) {
2543
2637
  return typeof u === "object" && u !== null && typeof u.value === "string" && typeof u.label === "string";
2544
2638
  }
2639
+ function parseQuantityOptions(options) {
2640
+ const units = Array.isArray(options.units) ? options.units.filter(isUnitOption) : [];
2641
+ const precision = typeof options.precision === "number" ? options.precision : void 0;
2642
+ const step = typeof options.step === "number" ? options.step : void 0;
2643
+ const unitPosition = options.unitPosition === "prefix" || options.unitPosition === "suffix" ? options.unitPosition : void 0;
2644
+ return { units, precision, step, unitPosition };
2645
+ }
2545
2646
  function QuantityWidget({
2546
2647
  id,
2547
2648
  name,
@@ -2560,10 +2661,12 @@ function QuantityWidget({
2560
2661
  options = {}
2561
2662
  }) {
2562
2663
  const error = firstError(rawErrors);
2563
- const units = Array.isArray(options.units) ? options.units.filter(isUnitOption) : [];
2564
- const precision = typeof options.precision === "number" ? options.precision : void 0;
2565
- const step = typeof options.step === "number" ? options.step : void 0;
2566
- const unitPositionOpt = options.unitPosition === "prefix" || options.unitPosition === "suffix" ? options.unitPosition : void 0;
2664
+ const {
2665
+ units,
2666
+ precision,
2667
+ step,
2668
+ unitPosition: unitPositionOpt
2669
+ } = parseQuantityOptions(options);
2567
2670
  const quantityValue = value != null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2568
2671
  const valueRef = useRef4(value);
2569
2672
  valueRef.current = value;
@@ -2585,6 +2688,7 @@ function QuantityWidget({
2585
2688
  min: propertySchema.minimum,
2586
2689
  max: propertySchema.maximum,
2587
2690
  label,
2691
+ labelPlacement: options?.labelPlacement,
2588
2692
  description,
2589
2693
  error,
2590
2694
  placeholder,
@@ -2596,9 +2700,309 @@ function QuantityWidget({
2596
2700
  );
2597
2701
  }
2598
2702
 
2703
+ // src/widgets/DateWidget.tsx
2704
+ import { DateInput } from "@petrarca/sonnet-ui";
2705
+ import { jsx as jsx26 } from "react/jsx-runtime";
2706
+ function resolvePrecision(value) {
2707
+ return value === "year" || value === "month" ? value : "day";
2708
+ }
2709
+ function DateWidget({
2710
+ id,
2711
+ value,
2712
+ onChange,
2713
+ onBlur,
2714
+ required,
2715
+ disabled,
2716
+ rawErrors,
2717
+ label,
2718
+ description,
2719
+ placeholder,
2720
+ options
2721
+ }) {
2722
+ const error = firstError(rawErrors);
2723
+ const precision = resolvePrecision(options?.precision);
2724
+ const locale = typeof options?.locale === "string" ? options.locale : void 0;
2725
+ return /* @__PURE__ */ jsx26(
2726
+ FormFieldWrapper,
2727
+ {
2728
+ inputId: id,
2729
+ label,
2730
+ description,
2731
+ required,
2732
+ labelPlacement: options?.labelPlacement,
2733
+ children: /* @__PURE__ */ jsx26(
2734
+ DateInput,
2735
+ {
2736
+ id,
2737
+ value: typeof value === "string" ? value : "",
2738
+ onChange,
2739
+ onBlur: () => onBlur?.(id, value),
2740
+ precision,
2741
+ locale,
2742
+ placeholder,
2743
+ required,
2744
+ disabled,
2745
+ error
2746
+ }
2747
+ )
2748
+ }
2749
+ );
2750
+ }
2751
+
2752
+ // src/widgets/TimeWidget.tsx
2753
+ import { TimeInput } from "@petrarca/sonnet-ui";
2754
+ import { jsx as jsx27 } from "react/jsx-runtime";
2755
+ function resolveGranularity(value) {
2756
+ return value === "second" ? "second" : "minute";
2757
+ }
2758
+ function TimeWidget({
2759
+ id,
2760
+ value,
2761
+ onChange,
2762
+ onBlur,
2763
+ required,
2764
+ disabled,
2765
+ rawErrors,
2766
+ label,
2767
+ description,
2768
+ placeholder,
2769
+ options
2770
+ }) {
2771
+ const error = firstError(rawErrors);
2772
+ const granularity = resolveGranularity(options?.granularity);
2773
+ return /* @__PURE__ */ jsx27(
2774
+ FormFieldWrapper,
2775
+ {
2776
+ inputId: id,
2777
+ label,
2778
+ description,
2779
+ required,
2780
+ labelPlacement: options?.labelPlacement,
2781
+ children: /* @__PURE__ */ jsx27(
2782
+ TimeInput,
2783
+ {
2784
+ id,
2785
+ value: typeof value === "string" ? value : "",
2786
+ onChange,
2787
+ onBlur: () => onBlur?.(id, value),
2788
+ granularity,
2789
+ placeholder,
2790
+ required,
2791
+ disabled,
2792
+ error
2793
+ }
2794
+ )
2795
+ }
2796
+ );
2797
+ }
2798
+
2799
+ // src/widgets/DateTimeWidget.tsx
2800
+ import { DateTimeInput } from "@petrarca/sonnet-ui";
2801
+ import { jsx as jsx28 } from "react/jsx-runtime";
2802
+ function resolveGranularity2(value) {
2803
+ return value === "second" ? "second" : "minute";
2804
+ }
2805
+ function DateTimeWidget({
2806
+ id,
2807
+ value,
2808
+ onChange,
2809
+ onBlur,
2810
+ required,
2811
+ disabled,
2812
+ rawErrors,
2813
+ label,
2814
+ description,
2815
+ options
2816
+ }) {
2817
+ const error = firstError(rawErrors);
2818
+ const granularity = resolveGranularity2(options?.granularity);
2819
+ const timeZone = typeof options?.timeZone === "string" ? options.timeZone : void 0;
2820
+ const locale = typeof options?.locale === "string" ? options.locale : void 0;
2821
+ return /* @__PURE__ */ jsx28(
2822
+ FormFieldWrapper,
2823
+ {
2824
+ inputId: id,
2825
+ label,
2826
+ description,
2827
+ required,
2828
+ labelPlacement: options?.labelPlacement,
2829
+ children: /* @__PURE__ */ jsx28(
2830
+ DateTimeInput,
2831
+ {
2832
+ idPrefix: id,
2833
+ value: typeof value === "string" ? value : "",
2834
+ onChange,
2835
+ onBlur: () => onBlur?.(id, value),
2836
+ granularity,
2837
+ timeZone,
2838
+ locale,
2839
+ required,
2840
+ disabled,
2841
+ error
2842
+ }
2843
+ )
2844
+ }
2845
+ );
2846
+ }
2847
+
2848
+ // src/widgets/DisplayWidget.tsx
2849
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
2850
+ function DisplayWidget({
2851
+ label,
2852
+ description
2853
+ }) {
2854
+ if (!label && !description) return null;
2855
+ return /* @__PURE__ */ jsxs17("div", { className: "space-y-1", children: [
2856
+ label && /* @__PURE__ */ jsx29("p", { className: "text-sm text-foreground", children: label }),
2857
+ description && /* @__PURE__ */ jsx29("p", { className: "text-sm text-muted-foreground", children: description })
2858
+ ] });
2859
+ }
2860
+
2861
+ // src/widgets/OneOfWidget.tsx
2862
+ import { useMemo as useMemo6 } from "react";
2863
+ import {
2864
+ getDiscriminator,
2865
+ getBranchValue,
2866
+ getBranchLabel,
2867
+ findActiveBranch
2868
+ } from "@petrarca/sonnet-core/schema";
2869
+ import {
2870
+ Label,
2871
+ Select,
2872
+ SelectTrigger,
2873
+ SelectValue,
2874
+ SelectContent,
2875
+ SelectItem
2876
+ } from "@petrarca/sonnet-ui";
2877
+ import { jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
2878
+ function buildChoices(branches, discriminator) {
2879
+ return branches.map((branch, index) => {
2880
+ const raw = discriminator ? getBranchValue(branch, discriminator) : void 0;
2881
+ return {
2882
+ index,
2883
+ value: raw !== void 0 ? String(raw) : String(index),
2884
+ label: getBranchLabel(branch, discriminator, index)
2885
+ };
2886
+ });
2887
+ }
2888
+ function buildBranchSchema(branch) {
2889
+ if (!branch.properties) return null;
2890
+ return {
2891
+ title: branch.title || "",
2892
+ type: "object",
2893
+ properties: branch.properties,
2894
+ required: branch.required,
2895
+ ...branch["x-ui-order"] ? { "x-ui-order": branch["x-ui-order"] } : {}
2896
+ };
2897
+ }
2898
+ function branchChangeData(choices, branches, discriminator, selected) {
2899
+ const choice = choices.find((c) => c.value === selected);
2900
+ if (!choice || !discriminator) return {};
2901
+ const discValue = getBranchValue(branches[choice.index], discriminator) ?? selected;
2902
+ return { [discriminator]: discValue };
2903
+ }
2904
+ function BranchSelector({
2905
+ id,
2906
+ label,
2907
+ description,
2908
+ choices,
2909
+ activeValue,
2910
+ disabled,
2911
+ onSelect
2912
+ }) {
2913
+ return /* @__PURE__ */ jsxs18("div", { className: "space-y-1", children: [
2914
+ label && /* @__PURE__ */ jsx30(Label, { htmlFor: id, children: label }),
2915
+ /* @__PURE__ */ jsxs18(Select, { value: activeValue, onValueChange: onSelect, disabled, children: [
2916
+ /* @__PURE__ */ jsx30(SelectTrigger, { id, children: /* @__PURE__ */ jsx30(SelectValue, { placeholder: "Select an option" }) }),
2917
+ /* @__PURE__ */ jsx30(SelectContent, { children: choices.map((c) => /* @__PURE__ */ jsx30(SelectItem, { value: c.value, children: c.label }, c.value)) })
2918
+ ] }),
2919
+ description && /* @__PURE__ */ jsx30("p", { className: "text-sm text-muted-foreground", children: description })
2920
+ ] });
2921
+ }
2922
+ function resolveActive(propertySchema, branches, choices, discriminator, data) {
2923
+ const index = findActiveBranch(propertySchema, data, discriminator);
2924
+ if (index < 0) return { value: "", label: "", subSchema: null };
2925
+ return {
2926
+ value: choices[index]?.value ?? "",
2927
+ label: choices[index]?.label ?? "",
2928
+ subSchema: buildBranchSchema(branches[index])
2929
+ };
2930
+ }
2931
+ function OneOfWidget({
2932
+ propertySchema,
2933
+ value,
2934
+ onChange,
2935
+ registry,
2936
+ uiSchema,
2937
+ disabled,
2938
+ readonly,
2939
+ label,
2940
+ description,
2941
+ id,
2942
+ options
2943
+ }) {
2944
+ const { renderer: Renderer, templates } = useNestedFormContext();
2945
+ const ContainerTemplate = templates.ObjectContainerTemplate ?? DefaultObjectContainerTemplate;
2946
+ const branches = useMemo6(() => propertySchema.oneOf ?? [], [propertySchema]);
2947
+ const discriminator = useMemo6(
2948
+ () => getDiscriminator(propertySchema),
2949
+ [propertySchema]
2950
+ );
2951
+ const choices = useMemo6(
2952
+ () => buildChoices(branches, discriminator),
2953
+ [branches, discriminator]
2954
+ );
2955
+ const data = value || {};
2956
+ const active = resolveActive(
2957
+ propertySchema,
2958
+ branches,
2959
+ choices,
2960
+ discriminator,
2961
+ data
2962
+ );
2963
+ if (branches.length === 0) return null;
2964
+ const subUiSchema = uiSchema?.["x-ui-fields"] ?? {};
2965
+ return /* @__PURE__ */ jsxs18("div", { className: "space-y-4", children: [
2966
+ /* @__PURE__ */ jsx30(
2967
+ BranchSelector,
2968
+ {
2969
+ id,
2970
+ label,
2971
+ description,
2972
+ choices,
2973
+ activeValue: active.value,
2974
+ disabled: disabled || readonly,
2975
+ onSelect: (selected) => onChange(branchChangeData(choices, branches, discriminator, selected))
2976
+ }
2977
+ ),
2978
+ active.subSchema && /* @__PURE__ */ jsx30(
2979
+ ContainerTemplate,
2980
+ {
2981
+ label: active.label,
2982
+ description: void 0,
2983
+ variant: options?.variant || "section",
2984
+ defaultOpen: true,
2985
+ children: /* @__PURE__ */ jsx30(
2986
+ Renderer,
2987
+ {
2988
+ schema: active.subSchema,
2989
+ data,
2990
+ onChange,
2991
+ disabled,
2992
+ readOnly: readonly,
2993
+ widgets: registry,
2994
+ uiSchema: subUiSchema
2995
+ }
2996
+ )
2997
+ }
2998
+ )
2999
+ ] });
3000
+ }
3001
+
2599
3002
  // src/widgets/widgetResolver.ts
2600
3003
  import {
2601
3004
  isNullableBoolean as isNullableBoolean2,
3005
+ isOneOf,
2602
3006
  extractEnumValues as extractEnumValues2,
2603
3007
  getFieldType
2604
3008
  } from "@petrarca/sonnet-core/schema";
@@ -2610,8 +3014,11 @@ var DEFAULT_WIDGET_MAP = {
2610
3014
  // Uses email input type
2611
3015
  uri: "TextInputWidget",
2612
3016
  // Uses url input type
2613
- url: "TextInputWidget"
3017
+ url: "TextInputWidget",
2614
3018
  // Uses url input type
3019
+ date: "DateWidget",
3020
+ "date-time": "DateTimeWidget",
3021
+ time: "TimeWidget"
2615
3022
  },
2616
3023
  number: {
2617
3024
  default: "NumberWidget"
@@ -2622,6 +3029,9 @@ var DEFAULT_WIDGET_MAP = {
2622
3029
  },
2623
3030
  boolean: {
2624
3031
  default: "CheckboxWidget"
3032
+ },
3033
+ null: {
3034
+ default: "DisplayWidget"
2625
3035
  }
2626
3036
  };
2627
3037
  function resolveWidgetByName(widgetName, propertySchema, registry) {
@@ -2671,6 +3081,7 @@ function registryLookup(registry, widgetName) {
2671
3081
  return registry[widgetName] || registry["TextInputWidget"];
2672
3082
  }
2673
3083
  function resolveFromSchemaStructure(schema, registry) {
3084
+ if (isOneOf(schema)) return registryLookup(registry, "OneOfWidget");
2674
3085
  if (isNullableBoolean2(schema))
2675
3086
  return registryLookup(registry, "CheckboxWidget");
2676
3087
  if (extractEnumValues2(schema).length > 0)
@@ -2714,6 +3125,11 @@ var DEFAULT_WIDGETS = {
2714
3125
  ObjectWidget,
2715
3126
  ArrayWidget,
2716
3127
  QuantityWidget,
3128
+ DateWidget,
3129
+ TimeWidget,
3130
+ DateTimeWidget,
3131
+ DisplayWidget,
3132
+ OneOfWidget,
2717
3133
  // Lowercase aliases for x-ui-widget convenience
2718
3134
  text: TextInputWidget,
2719
3135
  textarea: TextareaWidget,
@@ -2725,13 +3141,18 @@ var DEFAULT_WIDGETS = {
2725
3141
  "entity-select": EntitySelectWidget,
2726
3142
  object: ObjectWidget,
2727
3143
  array: ArrayWidget,
2728
- quantity: QuantityWidget
3144
+ quantity: QuantityWidget,
3145
+ date: DateWidget,
3146
+ time: TimeWidget,
3147
+ "date-time": DateTimeWidget,
3148
+ display: DisplayWidget,
3149
+ "one-of": OneOfWidget
2729
3150
  };
2730
3151
 
2731
3152
  // src/templates/HorizontalArrayItemTemplate.tsx
2732
3153
  import { Trash2 as Trash22, ChevronUp as ChevronUp2, ChevronDown as ChevronDown4 } from "lucide-react";
2733
3154
  import { Button as Button6 } from "@petrarca/sonnet-ui";
2734
- import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
3155
+ import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
2735
3156
  function HorizontalArrayItemTemplate({
2736
3157
  children,
2737
3158
  index,
@@ -2741,6 +3162,7 @@ function HorizontalArrayItemTemplate({
2741
3162
  disabled,
2742
3163
  layout,
2743
3164
  fieldCount,
3165
+ fid,
2744
3166
  onRemove,
2745
3167
  onMoveUp,
2746
3168
  onMoveDown
@@ -2754,9 +3176,10 @@ function HorizontalArrayItemTemplate({
2754
3176
  );
2755
3177
  const gap = resolvedLayout.gap ?? 8;
2756
3178
  const alignItems = resolvedLayout.compact ? "center" : "start";
2757
- return /* @__PURE__ */ jsxs17(
3179
+ return /* @__PURE__ */ jsxs19(
2758
3180
  "div",
2759
3181
  {
3182
+ "data-fid": fid,
2760
3183
  style: {
2761
3184
  display: "grid",
2762
3185
  gridTemplateColumns: template,
@@ -2765,8 +3188,8 @@ function HorizontalArrayItemTemplate({
2765
3188
  },
2766
3189
  children: [
2767
3190
  children,
2768
- orderable && /* @__PURE__ */ jsxs17("div", { className: "flex flex-col items-center justify-center", children: [
2769
- /* @__PURE__ */ jsx26(
3191
+ orderable && /* @__PURE__ */ jsxs19("div", { className: "flex flex-col items-center justify-center", children: [
3192
+ /* @__PURE__ */ jsx31(
2770
3193
  Button6,
2771
3194
  {
2772
3195
  type: "button",
@@ -2776,10 +3199,10 @@ function HorizontalArrayItemTemplate({
2776
3199
  onClick: onMoveUp,
2777
3200
  title: "Move up",
2778
3201
  className: "h-4 w-8 p-0",
2779
- children: /* @__PURE__ */ jsx26(ChevronUp2, { className: "h-3 w-3" })
3202
+ children: /* @__PURE__ */ jsx31(ChevronUp2, { className: "h-3 w-3" })
2780
3203
  }
2781
3204
  ),
2782
- /* @__PURE__ */ jsx26(
3205
+ /* @__PURE__ */ jsx31(
2783
3206
  Button6,
2784
3207
  {
2785
3208
  type: "button",
@@ -2789,11 +3212,11 @@ function HorizontalArrayItemTemplate({
2789
3212
  onClick: onMoveDown,
2790
3213
  title: "Move down",
2791
3214
  className: "h-4 w-8 p-0",
2792
- children: /* @__PURE__ */ jsx26(ChevronDown4, { className: "h-3 w-3" })
3215
+ children: /* @__PURE__ */ jsx31(ChevronDown4, { className: "h-3 w-3" })
2793
3216
  }
2794
3217
  )
2795
3218
  ] }),
2796
- canRemove && /* @__PURE__ */ jsx26(
3219
+ canRemove && /* @__PURE__ */ jsx31(
2797
3220
  Button6,
2798
3221
  {
2799
3222
  type: "button",
@@ -2803,7 +3226,7 @@ function HorizontalArrayItemTemplate({
2803
3226
  onClick: onRemove,
2804
3227
  title: "Remove item",
2805
3228
  className: "text-muted-foreground hover:text-destructive h-8 w-8 p-0",
2806
- children: /* @__PURE__ */ jsx26(Trash22, { className: "h-3.5 w-3.5" })
3229
+ children: /* @__PURE__ */ jsx31(Trash22, { className: "h-3.5 w-3.5" })
2807
3230
  }
2808
3231
  )
2809
3232
  ]
@@ -2811,21 +3234,132 @@ function HorizontalArrayItemTemplate({
2811
3234
  );
2812
3235
  }
2813
3236
 
3237
+ // src/templates/TabsGroupTemplate.tsx
3238
+ import { useState as useState7 } from "react";
3239
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@petrarca/sonnet-ui";
3240
+
3241
+ // src/templates/GroupErrorBadge.tsx
3242
+ import { AlertCircle } from "lucide-react";
3243
+ import { jsx as jsx32 } from "react/jsx-runtime";
3244
+ function GroupErrorBadge({
3245
+ count
3246
+ }) {
3247
+ if (count === 0) return null;
3248
+ const label = count === 1 ? "1 field needs attention" : `${count} fields need attention`;
3249
+ return /* @__PURE__ */ jsx32(
3250
+ "span",
3251
+ {
3252
+ role: "img",
3253
+ "aria-label": label,
3254
+ title: label,
3255
+ className: "inline-flex shrink-0 text-destructive",
3256
+ children: /* @__PURE__ */ jsx32(AlertCircle, { className: "h-3.5 w-3.5" })
3257
+ }
3258
+ );
3259
+ }
3260
+
3261
+ // src/templates/TabsGroupTemplate.tsx
3262
+ import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
3263
+ function TabsGroupTemplate({
3264
+ groups
3265
+ }) {
3266
+ const [active, setActive] = useState7(groups[0].key);
3267
+ const value = groups.some((g) => g.key === active) ? active : groups[0].key;
3268
+ return /* @__PURE__ */ jsxs20(Tabs, { value, onValueChange: setActive, children: [
3269
+ /* @__PURE__ */ jsx33(TabsList, { className: "mb-4", children: groups.map((g) => /* @__PURE__ */ jsxs20(TabsTrigger, { value: g.key, className: "gap-1.5", children: [
3270
+ g.label,
3271
+ /* @__PURE__ */ jsx33(GroupErrorBadge, { count: g.errorCount })
3272
+ ] }, g.key)) }),
3273
+ groups.map((g) => /* @__PURE__ */ jsx33(TabsContent, { value: g.key, className: "space-y-4", children: g.content }, g.key))
3274
+ ] });
3275
+ }
3276
+
3277
+ // src/templates/StepperGroupTemplate.tsx
3278
+ import { useState as useState8 } from "react";
3279
+ import {
3280
+ Button as Button7,
3281
+ Stepper,
3282
+ StepperList,
3283
+ StepperItem,
3284
+ StepperTrigger,
3285
+ StepperIndicator,
3286
+ StepperSeparator,
3287
+ StepperTitle,
3288
+ StepperContent,
3289
+ StepperPrev,
3290
+ StepperNext
3291
+ } from "@petrarca/sonnet-ui";
3292
+ import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
3293
+ function StepperGroupTemplate({
3294
+ groups
3295
+ }) {
3296
+ const [active, setActive] = useState8(groups[0].key);
3297
+ const value = groups.some((g) => g.key === active) ? active : groups[0].key;
3298
+ const isLast = value === groups[groups.length - 1].key;
3299
+ return /* @__PURE__ */ jsxs21(Stepper, { value, onValueChange: setActive, children: [
3300
+ /* @__PURE__ */ jsx34(StepperList, { className: "mb-4 gap-2", children: groups.map((g) => /* @__PURE__ */ jsxs21(StepperItem, { value: g.key, children: [
3301
+ /* @__PURE__ */ jsxs21(StepperTrigger, { children: [
3302
+ /* @__PURE__ */ jsx34(StepperIndicator, {}),
3303
+ /* @__PURE__ */ jsxs21(StepperTitle, { className: "inline-flex items-center gap-1.5", children: [
3304
+ g.label,
3305
+ /* @__PURE__ */ jsx34(GroupErrorBadge, { count: g.errorCount })
3306
+ ] })
3307
+ ] }),
3308
+ /* @__PURE__ */ jsx34(StepperSeparator, {})
3309
+ ] }, g.key)) }),
3310
+ groups.map((g) => /* @__PURE__ */ jsx34(StepperContent, { value: g.key, className: "space-y-4", children: g.content }, g.key)),
3311
+ /* @__PURE__ */ jsxs21("div", { className: "mt-4 flex justify-between", children: [
3312
+ /* @__PURE__ */ jsx34(StepperPrev, { asChild: true, children: /* @__PURE__ */ jsx34(Button7, { variant: "outline", size: "sm", children: "Back" }) }),
3313
+ !isLast && /* @__PURE__ */ jsx34(StepperNext, { asChild: true, children: /* @__PURE__ */ jsx34(Button7, { size: "sm", children: "Next" }) })
3314
+ ] })
3315
+ ] });
3316
+ }
3317
+
3318
+ // src/templates/SectionsGroupTemplate.tsx
3319
+ import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
3320
+ function SectionsGroupTemplate({
3321
+ groups
3322
+ }) {
3323
+ return /* @__PURE__ */ jsx35("div", { className: "space-y-6", children: groups.map((g) => /* @__PURE__ */ jsxs22(
3324
+ "fieldset",
3325
+ {
3326
+ className: "rounded-md border border-border/60 px-3 pb-3 pt-1",
3327
+ children: [
3328
+ /* @__PURE__ */ jsxs22("legend", { className: "inline-flex items-center gap-1.5 px-1 text-sm font-medium text-muted-foreground", children: [
3329
+ g.label,
3330
+ /* @__PURE__ */ jsx35(GroupErrorBadge, { count: g.errorCount })
3331
+ ] }),
3332
+ /* @__PURE__ */ jsx35("div", { className: "space-y-4", children: g.content })
3333
+ ]
3334
+ },
3335
+ g.key
3336
+ )) });
3337
+ }
3338
+
2814
3339
  // src/templates/index.ts
2815
3340
  var DEFAULT_TEMPLATES = {
2816
3341
  ArrayItemTemplate: DefaultArrayItemTemplate,
2817
3342
  HorizontalArrayItemTemplate,
2818
3343
  ArrayHeaderTemplate: DefaultArrayHeaderTemplate,
2819
- ObjectContainerTemplate: DefaultObjectContainerTemplate
3344
+ ObjectContainerTemplate: DefaultObjectContainerTemplate,
3345
+ TabsGroupTemplate,
3346
+ StepperGroupTemplate,
3347
+ SectionsGroupTemplate
2820
3348
  };
3349
+ function groupTemplateKey(variant) {
3350
+ if (!variant) return "TabsGroupTemplate";
3351
+ const name = variant.charAt(0).toUpperCase() + variant.slice(1);
3352
+ return `${name}GroupTemplate`;
3353
+ }
2821
3354
 
2822
3355
  // src/FormFieldRenderer.tsx
3356
+ import { warnLog } from "@petrarca/sonnet-core";
2823
3357
  import {
2824
3358
  getFieldTitle,
2825
3359
  getFieldHelpText,
2826
3360
  getFieldPlaceholder
2827
3361
  } from "@petrarca/sonnet-core/schema";
2828
- import { jsx as jsx27 } from "react/jsx-runtime";
3362
+ import { jsx as jsx36 } from "react/jsx-runtime";
2829
3363
  function mergeFieldOptions(globalOptions, property, uiSchema) {
2830
3364
  const xWidgetOptions = property["x-ui-options"] || {};
2831
3365
  const fieldOptions = uiSchema?.["x-ui-options"] || {};
@@ -2837,6 +3371,29 @@ function resolveEffectiveLabel(label, uiTitle, globalLabelOption) {
2837
3371
  if (uiTitle === false) return "";
2838
3372
  return label;
2839
3373
  }
3374
+ var FIELD_WIDTH_CLASS = {
3375
+ sm: "w-32",
3376
+ md: "w-48",
3377
+ lg: "w-64",
3378
+ full: "w-full"
3379
+ };
3380
+ function applyFieldWidth(widget, uiSchema, mergedOptions) {
3381
+ const candidate = uiSchema?.["x-ui-width"] ?? mergedOptions.width;
3382
+ if (candidate && !(candidate in FIELD_WIDTH_CLASS)) {
3383
+ warnLog(
3384
+ `[FormFieldRenderer] Unknown x-ui-width value: "${candidate}". Falling back to "full".`
3385
+ );
3386
+ }
3387
+ const width = candidate && candidate in FIELD_WIDTH_CLASS ? candidate : "full";
3388
+ if (width === "full") return widget;
3389
+ return /* @__PURE__ */ jsx36("div", { className: FIELD_WIDTH_CLASS[width], children: widget });
3390
+ }
3391
+ function resolvePresentationText(property, uiSchema) {
3392
+ return {
3393
+ description: uiSchema?.["x-ui-description"] ?? getFieldHelpText(property),
3394
+ placeholder: uiSchema?.["x-ui-placeholder"] ?? getFieldPlaceholder(property)
3395
+ };
3396
+ }
2840
3397
  function resolveFieldFlags(fieldOptions, globalOptions, disabled, readOnly) {
2841
3398
  return {
2842
3399
  disabled: fieldOptions.disabled ?? globalOptions.disabled ?? disabled,
@@ -2856,11 +3413,14 @@ function FormFieldRenderer({
2856
3413
  uiSchema,
2857
3414
  registry,
2858
3415
  globalOptions = {},
2859
- formData
3416
+ formData,
3417
+ idPrefix = "field"
2860
3418
  }) {
2861
3419
  const label = getFieldTitle(fieldName, property);
2862
- const description = uiSchema?.["x-ui-description"] ?? getFieldHelpText(property);
2863
- const placeholder = uiSchema?.["x-ui-placeholder"] ?? getFieldPlaceholder(property);
3420
+ const { description, placeholder } = resolvePresentationText(
3421
+ property,
3422
+ uiSchema
3423
+ );
2864
3424
  const Widget = resolveWidget(property, uiSchema, registry);
2865
3425
  const mergedOptions = mergeFieldOptions(globalOptions, property, uiSchema);
2866
3426
  if (mergedOptions.hidden) return null;
@@ -2876,10 +3436,10 @@ function FormFieldRenderer({
2876
3436
  uiSchema?.["x-ui-title"],
2877
3437
  globalOptions.label
2878
3438
  );
2879
- return /* @__PURE__ */ jsx27(
3439
+ const widget = /* @__PURE__ */ jsx36(
2880
3440
  Widget,
2881
3441
  {
2882
- id: `field-${fieldName}`,
3442
+ id: `${idPrefix}-${fieldName}`,
2883
3443
  name: fieldName,
2884
3444
  value,
2885
3445
  onChange,
@@ -2898,10 +3458,11 @@ function FormFieldRenderer({
2898
3458
  formData
2899
3459
  }
2900
3460
  );
3461
+ return applyFieldWidth(widget, uiSchema, mergedOptions);
2901
3462
  }
2902
3463
 
2903
3464
  // src/hooks/useFormEngine.ts
2904
- import { useState as useState7, useEffect as useEffect3, useMemo as useMemo6, useCallback as useCallback7, useRef as useRef5 } from "react";
3465
+ import { useState as useState9, useEffect as useEffect3, useMemo as useMemo7, useCallback as useCallback7, useRef as useRef5 } from "react";
2905
3466
  import { isEqual } from "@petrarca/sonnet-core";
2906
3467
  import {
2907
3468
  getSchemaProperties,
@@ -2968,14 +3529,19 @@ function useFormEngine({
2968
3529
  readOnly = false,
2969
3530
  showActions = false,
2970
3531
  deferChanges = false,
2971
- deferStateUpdates = false
3532
+ deferStateUpdates = false,
3533
+ allowPristineSave = false,
3534
+ refResolver
2972
3535
  }) {
2973
- const refResolvedSchema = useMemo6(() => resolveRefs(schema), [schema]);
2974
- const [originalData, setOriginalData] = useState7(
3536
+ const refResolvedSchema = useMemo7(
3537
+ () => resolveRefs(schema, refResolver),
3538
+ [schema, refResolver]
3539
+ );
3540
+ const [originalData, setOriginalData] = useState9(
2975
3541
  () => applySchemaDefaults2(data, refResolvedSchema)
2976
3542
  );
2977
- const [originalDataInitialized, setOriginalDataInitialized] = useState7(false);
2978
- const [nonSchemaData, setNonSchemaData] = useState7(
3543
+ const [originalDataInitialized, setOriginalDataInitialized] = useState9(false);
3544
+ const [nonSchemaData, setNonSchemaData] = useState9(
2979
3545
  () => {
2980
3546
  const props = getSchemaProperties(refResolvedSchema);
2981
3547
  const names = Object.keys(props);
@@ -2984,26 +3550,23 @@ function useFormEngine({
2984
3550
  );
2985
3551
  }
2986
3552
  );
2987
- const [formData, setFormData] = useState7(
3553
+ const [formData, setFormData] = useState9(
2988
3554
  () => applySchemaDefaults2(data, refResolvedSchema)
2989
3555
  );
2990
- const [pendingChanges, setPendingChanges] = useState7({});
2991
- const resolvedSchema = useMemo6(
3556
+ const formDataRef = useRef5(formData);
3557
+ formDataRef.current = formData;
3558
+ const [pendingChanges, setPendingChanges] = useState9({});
3559
+ const resolvedSchema = useMemo7(
2992
3560
  () => resolveSchema(refResolvedSchema, formData),
2993
3561
  [refResolvedSchema, formData]
2994
3562
  );
2995
- const orderedProperties = useMemo6(() => {
3563
+ const orderedProperties = useMemo7(() => {
2996
3564
  const properties2 = getSchemaProperties(resolvedSchema);
2997
3565
  const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
2998
3566
  const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
2999
3567
  return orderProperties(properties2, orderArray);
3000
3568
  }, [resolvedSchema, uiSchema]);
3001
- const [isValid, setIsValid] = useState7(() => {
3002
- const initResolved = resolveSchema(refResolvedSchema, data);
3003
- const validation = validateFormData(initResolved, data);
3004
- return validation.isValid;
3005
- });
3006
- const [initialDefaultsSent, setInitialDefaultsSent] = useState7(false);
3569
+ const [initialDefaultsSent, setInitialDefaultsSent] = useState9(false);
3007
3570
  const properties = getSchemaProperties(resolvedSchema);
3008
3571
  useEffect3(() => {
3009
3572
  if (!initialDefaultsSent) {
@@ -3050,8 +3613,6 @@ function useFormEngine({
3050
3613
  setOriginalDataInitialized(true);
3051
3614
  }
3052
3615
  }
3053
- const validation = validateFormData(resolvedSchema, newData);
3054
- setIsValid(validation.isValid);
3055
3616
  }, [
3056
3617
  data,
3057
3618
  refResolvedSchema,
@@ -3060,21 +3621,31 @@ function useFormEngine({
3060
3621
  originalData,
3061
3622
  originalDataInitialized
3062
3623
  ]);
3063
- const hasChanges = useMemo6(
3624
+ const hasChanges = useMemo7(
3064
3625
  () => !isEqual(formData, originalData),
3065
3626
  [formData, originalData]
3066
3627
  );
3067
- const displayData = useMemo6(() => {
3628
+ const displayData = useMemo7(() => {
3068
3629
  if (deferStateUpdates && Object.keys(pendingChanges).length > 0) {
3069
3630
  return { ...formData, ...pendingChanges };
3070
3631
  }
3071
3632
  return formData;
3072
3633
  }, [formData, pendingChanges, deferStateUpdates]);
3634
+ const validation = useMemo7(
3635
+ () => validateFormData(resolvedSchema, displayData),
3636
+ [resolvedSchema, displayData]
3637
+ );
3638
+ const { isValid, fieldErrors } = validation;
3639
+ const onValidationChangeRef = useRef5(onValidationChange);
3640
+ onValidationChangeRef.current = onValidationChange;
3641
+ useEffect3(() => {
3642
+ onValidationChangeRef.current?.(validation.errors, validation.isValid);
3643
+ }, [validation]);
3073
3644
  const handleFieldChange = useCallback7(
3074
3645
  (fieldName, value) => {
3075
3646
  if (readOnly) return;
3076
3647
  const immediate = isImmediateField(fieldName, properties);
3077
- if (showActions && deferStateUpdates && !immediate) {
3648
+ if (deferStateUpdates && !immediate) {
3078
3649
  setPendingChanges((prev) => ({ ...prev, [fieldName]: value }));
3079
3650
  return;
3080
3651
  }
@@ -3084,11 +3655,6 @@ function useFormEngine({
3084
3655
  const shouldCallOnChange = !deferChanges || immediate;
3085
3656
  if (shouldCallOnChange && onChange) onChange(newData);
3086
3657
  }
3087
- const validation = validateFormData(resolvedSchema, newData);
3088
- setIsValid(validation.isValid);
3089
- if (onValidationChange) {
3090
- onValidationChange(validation.errors, validation.isValid);
3091
- }
3092
3658
  },
3093
3659
  [
3094
3660
  readOnly,
@@ -3097,14 +3663,12 @@ function useFormEngine({
3097
3663
  deferStateUpdates,
3098
3664
  showActions,
3099
3665
  onChange,
3100
- resolvedSchema,
3101
- onValidationChange,
3102
3666
  properties
3103
3667
  ]
3104
3668
  );
3105
3669
  const flushPendingField = useCallback7(
3106
3670
  (fieldName) => {
3107
- if (!showActions || !deferStateUpdates) return;
3671
+ if (!deferStateUpdates) return;
3108
3672
  if (pendingChanges[fieldName] === void 0) return;
3109
3673
  const newData = { ...formData, ...pendingChanges };
3110
3674
  setFormData(newData);
@@ -3113,37 +3677,26 @@ function useFormEngine({
3113
3677
  delete updated[fieldName];
3114
3678
  return updated;
3115
3679
  });
3116
- const validation = validateFormData(resolvedSchema, newData);
3117
- setIsValid(validation.isValid);
3118
- if (onValidationChange) {
3119
- onValidationChange(validation.errors, validation.isValid);
3120
- }
3680
+ if (!showActions && onChange) onChange(newData);
3121
3681
  },
3122
- [
3123
- showActions,
3124
- deferStateUpdates,
3125
- pendingChanges,
3126
- formData,
3127
- resolvedSchema,
3128
- onValidationChange
3129
- ]
3682
+ [showActions, deferStateUpdates, pendingChanges, formData, onChange]
3130
3683
  );
3131
3684
  const handleFieldBlur = useCallback7(
3132
3685
  (fieldName) => {
3133
3686
  if (readOnly) return;
3134
3687
  flushPendingField(fieldName);
3688
+ const latest = formDataRef.current;
3135
3689
  if (!showActions && deferChanges && onChange) {
3136
- onChange(formData);
3690
+ onChange(latest);
3137
3691
  }
3138
3692
  if (onBlur) {
3139
3693
  const hasPending = deferStateUpdates && pendingChanges[fieldName] !== void 0;
3140
- const dataToPass = hasPending ? { ...formData, [fieldName]: pendingChanges[fieldName] } : formData;
3694
+ const dataToPass = hasPending ? { ...latest, [fieldName]: pendingChanges[fieldName] } : latest;
3141
3695
  onBlur(fieldName, dataToPass);
3142
3696
  }
3143
3697
  },
3144
3698
  [
3145
3699
  readOnly,
3146
- formData,
3147
3700
  pendingChanges,
3148
3701
  deferChanges,
3149
3702
  deferStateUpdates,
@@ -3154,11 +3707,11 @@ function useFormEngine({
3154
3707
  ]
3155
3708
  );
3156
3709
  const handleSave = useCallback7(() => {
3157
- if (!hasChanges) return;
3710
+ if (!hasChanges && !allowPristineSave) return;
3158
3711
  const finalData = deferStateUpdates && Object.keys(pendingChanges).length > 0 ? { ...formData, ...pendingChanges } : formData;
3159
3712
  const completeData = { ...nonSchemaData, ...finalData };
3160
- const validation = validateFormData(resolvedSchema, finalData);
3161
- if (validation.isValid && onUpdate) {
3713
+ const validation2 = validateFormData(resolvedSchema, finalData);
3714
+ if (validation2.isValid && onUpdate) {
3162
3715
  const changed = {};
3163
3716
  for (const key of Object.keys(finalData)) {
3164
3717
  if (!isEqual(finalData[key], originalData[key])) {
@@ -3171,6 +3724,7 @@ function useFormEngine({
3171
3724
  }
3172
3725
  }, [
3173
3726
  hasChanges,
3727
+ allowPristineSave,
3174
3728
  resolvedSchema,
3175
3729
  formData,
3176
3730
  originalData,
@@ -3191,6 +3745,7 @@ function useFormEngine({
3191
3745
  orderedProperties,
3192
3746
  hasChanges,
3193
3747
  isValid,
3748
+ fieldErrors,
3194
3749
  handleFieldChange,
3195
3750
  handleFieldBlur,
3196
3751
  handleSave,
@@ -3199,7 +3754,7 @@ function useFormEngine({
3199
3754
  }
3200
3755
 
3201
3756
  // src/JsonSchemaFormRenderer.tsx
3202
- import { jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
3757
+ import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
3203
3758
  function stripFids(data) {
3204
3759
  const result = {};
3205
3760
  for (const [key, value] of Object.entries(data)) {
@@ -3232,7 +3787,8 @@ function resolveFlagDefaults(props) {
3232
3787
  deferStateUpdates: props.deferStateUpdates ?? false,
3233
3788
  showCancel: props.showCancel ?? true,
3234
3789
  showExtraProperties: props.showExtraProperties ?? true,
3235
- displayContents: props.displayContents ?? false
3790
+ displayContents: props.displayContents ?? false,
3791
+ allowPristineSave: props.allowPristineSave ?? false
3236
3792
  };
3237
3793
  }
3238
3794
  function resolveValueDefaults(props) {
@@ -3265,54 +3821,147 @@ function useWrappedCallbacks(onChange, onBlur, onUpdate) {
3265
3821
  function buildFieldMap(fields) {
3266
3822
  const map = /* @__PURE__ */ new Map();
3267
3823
  for (const field of fields) {
3268
- if (React11.isValidElement(field) && field.key != null) {
3824
+ if (React13.isValidElement(field) && field.key != null) {
3269
3825
  map.set(String(field.key), field);
3270
3826
  }
3271
3827
  }
3272
3828
  return map;
3273
3829
  }
3830
+ var COLUMN_TRACK = WIDTH_TOKEN_REMS;
3831
+ function resolveGridColumns(row, fieldCount) {
3832
+ if (row.columns) return row.columns;
3833
+ if (row.widths && row.widths.length > 0) {
3834
+ return Array.from(
3835
+ { length: fieldCount },
3836
+ (_, i) => COLUMN_TRACK[row.widths?.[i] ?? "full"]
3837
+ ).join(" ");
3838
+ }
3839
+ return `repeat(${fieldCount}, 1fr)`;
3840
+ }
3841
+ function wrapInGroup(group, key, content) {
3842
+ if (!group) return content;
3843
+ return /* @__PURE__ */ jsxs23(
3844
+ "fieldset",
3845
+ {
3846
+ className: "rounded-md border border-border/60 px-3 pb-3 pt-1",
3847
+ children: [
3848
+ /* @__PURE__ */ jsx37("legend", { className: "px-1 text-sm font-medium text-muted-foreground", children: group }),
3849
+ content
3850
+ ]
3851
+ },
3852
+ `group:${key}`
3853
+ );
3854
+ }
3274
3855
  function buildRowElement(row, fieldMap, defaultRowGap) {
3275
3856
  const rowFields = row.fields.map((name) => fieldMap.get(name)).filter((el) => el != null);
3276
3857
  if (rowFields.length === 0) return null;
3277
- const gridColumns = row.columns ?? `repeat(${rowFields.length}, 1fr)`;
3278
3858
  const rowGap = row.gap ?? defaultRowGap;
3859
+ const rowKey = row.fields.join(",");
3860
+ const style = row.arrange === "flow" ? {
3861
+ display: "flex",
3862
+ flexWrap: "wrap",
3863
+ alignItems: "center",
3864
+ gap: `${rowGap}px`
3865
+ } : {
3866
+ display: "grid",
3867
+ gridTemplateColumns: resolveGridColumns(row, rowFields.length),
3868
+ gap: `${rowGap}px`
3869
+ };
3870
+ const inner = /* @__PURE__ */ jsx37("div", { style, children: rowFields }, `row:${rowKey}`);
3279
3871
  return {
3280
- element: /* @__PURE__ */ jsx28(
3281
- "div",
3282
- {
3283
- style: {
3284
- display: "grid",
3285
- gridTemplateColumns: gridColumns,
3286
- gap: `${rowGap}px`
3287
- },
3288
- children: rowFields
3289
- },
3290
- `row:${row.fields.join(",")}`
3291
- ),
3872
+ element: wrapInGroup(row.group, rowKey, inner),
3292
3873
  placed: row.fields
3293
3874
  };
3294
3875
  }
3295
3876
  function collectUnplacedFields(fields, placed) {
3296
3877
  const result = [];
3297
3878
  for (const field of fields) {
3298
- if (React11.isValidElement(field) && field.key != null && !placed.has(String(field.key))) {
3879
+ if (React13.isValidElement(field) && field.key != null && !placed.has(String(field.key))) {
3299
3880
  result.push(field);
3300
3881
  }
3301
3882
  }
3302
3883
  return result;
3303
3884
  }
3304
- function layoutFieldsInRows(fields, rowDefs, defaultRowGap) {
3305
- const fieldMap = buildFieldMap(fields);
3306
- const placed = /* @__PURE__ */ new Set();
3885
+ function buildRows(rowDefs, fieldMap, defaultRowGap, placed) {
3307
3886
  const elements = [];
3308
3887
  for (const row of rowDefs) {
3309
3888
  const result = buildRowElement(row, fieldMap, defaultRowGap);
3310
- if (result == null) continue;
3311
- elements.push(result.element);
3312
- for (const name of result.placed) placed.add(name);
3889
+ for (const name of row.fields) placed.add(name);
3890
+ if (result != null) elements.push(result.element);
3313
3891
  }
3892
+ return elements;
3893
+ }
3894
+ function layoutFieldsInRows(fields, rowDefs, defaultRowGap) {
3895
+ const placed = /* @__PURE__ */ new Set();
3896
+ const elements = buildRows(
3897
+ rowDefs,
3898
+ buildFieldMap(fields),
3899
+ defaultRowGap,
3900
+ placed
3901
+ );
3314
3902
  return elements.concat(collectUnplacedFields(fields, placed));
3315
3903
  }
3904
+ function pointerRoot(pointer) {
3905
+ const token = pointer.split("/")[1] ?? "";
3906
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
3907
+ }
3908
+ function groupErrorCount(tab, fieldErrors) {
3909
+ if (fieldErrors.length === 0) return 0;
3910
+ const owned = new Set(tab.rows.flatMap((row) => row.fields));
3911
+ return fieldErrors.filter((e) => owned.has(pointerRoot(e.pointer))).length;
3912
+ }
3913
+ function groupVisibility(groupDefs, formData) {
3914
+ const visible = groupDefs.map(
3915
+ (tab) => evaluateUiRule(tab.rule, formData).visible
3916
+ );
3917
+ return { visible, allHidden: !visible.some(Boolean) };
3918
+ }
3919
+ function hiddenGroupFieldNames(groupDefs, formData) {
3920
+ const hidden = /* @__PURE__ */ new Set();
3921
+ const { visible, allHidden } = groupVisibility(groupDefs, formData);
3922
+ if (allHidden) return hidden;
3923
+ groupDefs.forEach((tab, i) => {
3924
+ if (visible[i]) return;
3925
+ for (const row of tab.rows) for (const f of row.fields) hidden.add(f);
3926
+ });
3927
+ return hidden;
3928
+ }
3929
+ function resolveGroupTemplate(templates, variant) {
3930
+ const registered = templates[groupTemplateKey(variant)];
3931
+ if (typeof registered === "function") {
3932
+ return registered;
3933
+ }
3934
+ return DEFAULT_TEMPLATES.TabsGroupTemplate;
3935
+ }
3936
+ function HiddenErrorNotice({
3937
+ count
3938
+ }) {
3939
+ if (count === 0) return null;
3940
+ return /* @__PURE__ */ jsxs23("p", { className: "flex items-center gap-1.5 text-sm text-destructive", children: [
3941
+ /* @__PURE__ */ jsx37(AlertCircle2, { className: "h-4 w-4 shrink-0" }),
3942
+ count === 1 ? "1 required field is in a section that is not currently shown." : `${count} required fields are in sections that are not currently shown.`
3943
+ ] });
3944
+ }
3945
+ function layoutFieldsInGroups(fields, groupDefs, defaultRowGap, fieldErrors, formData, GroupTemplate) {
3946
+ const fieldMap = buildFieldMap(fields);
3947
+ const placed = /* @__PURE__ */ new Set();
3948
+ const { visible, allHidden } = groupVisibility(groupDefs, formData);
3949
+ const built = groupDefs.map((tab, index) => ({
3950
+ index,
3951
+ rows: buildRows(tab.rows, fieldMap, defaultRowGap, placed)
3952
+ }));
3953
+ const unplaced = collectUnplacedFields(fields, placed);
3954
+ if (allHidden) {
3955
+ return [...built.flatMap((b) => b.rows), ...unplaced];
3956
+ }
3957
+ const groups = built.filter((b) => visible[b.index]).map((b) => ({
3958
+ key: `group-${b.index}`,
3959
+ label: groupDefs[b.index].label,
3960
+ content: b.rows,
3961
+ errorCount: groupErrorCount(groupDefs[b.index], fieldErrors)
3962
+ }));
3963
+ return [/* @__PURE__ */ jsx37(GroupTemplate, { groups }, "form:groups"), ...unplaced];
3964
+ }
3316
3965
  function FormContainer({
3317
3966
  displayContents,
3318
3967
  showExtraProperties,
@@ -3328,15 +3977,17 @@ function FormContainer({
3328
3977
  cancelLabel,
3329
3978
  showCancel,
3330
3979
  leadingActions,
3980
+ hiddenErrorCount,
3331
3981
  children
3332
3982
  }) {
3333
3983
  if (displayContents) {
3334
- return /* @__PURE__ */ jsx28("div", { style: { display: "contents" }, children });
3984
+ return /* @__PURE__ */ jsx37("div", { style: { display: "contents" }, children });
3335
3985
  }
3336
- return /* @__PURE__ */ jsxs18(SimpleStack, { gap: 24, children: [
3986
+ return /* @__PURE__ */ jsxs23(SimpleStack, { gap: "md", children: [
3337
3987
  children,
3338
- showExtraProperties && /* @__PURE__ */ jsx28(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
3339
- showActions && /* @__PURE__ */ jsx28(
3988
+ showExtraProperties && /* @__PURE__ */ jsx37(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
3989
+ /* @__PURE__ */ jsx37(HiddenErrorNotice, { count: hiddenErrorCount }),
3990
+ showActions && /* @__PURE__ */ jsx37(
3340
3991
  FormActions,
3341
3992
  {
3342
3993
  onSave: handleSave,
@@ -3362,7 +4013,8 @@ function JsonSchemaFormRenderer(props) {
3362
4013
  onUpdate,
3363
4014
  onCancel,
3364
4015
  templates,
3365
- leadingActions
4016
+ leadingActions,
4017
+ idPrefix = "field"
3366
4018
  } = props;
3367
4019
  const {
3368
4020
  disabled,
@@ -3372,7 +4024,8 @@ function JsonSchemaFormRenderer(props) {
3372
4024
  deferStateUpdates,
3373
4025
  showCancel,
3374
4026
  showExtraProperties,
3375
- displayContents
4027
+ displayContents,
4028
+ allowPristineSave
3376
4029
  } = resolveFlagDefaults(props);
3377
4030
  const { saveLabel, cancelLabel, widgets, uiSchema } = resolveValueDefaults(props);
3378
4031
  const { wrappedOnChange, wrappedOnBlur, wrappedOnUpdate } = useWrappedCallbacks(onChange, onBlur, onUpdate);
@@ -3383,6 +4036,7 @@ function JsonSchemaFormRenderer(props) {
3383
4036
  orderedProperties,
3384
4037
  hasChanges,
3385
4038
  isValid,
4039
+ fieldErrors,
3386
4040
  handleFieldChange,
3387
4041
  handleFieldBlur,
3388
4042
  handleSave,
@@ -3399,15 +4053,24 @@ function JsonSchemaFormRenderer(props) {
3399
4053
  readOnly,
3400
4054
  showActions,
3401
4055
  deferChanges,
3402
- deferStateUpdates
4056
+ deferStateUpdates,
4057
+ allowPristineSave,
4058
+ refResolver: props.refResolver
3403
4059
  });
3404
- const globalOptions = uiSchema["x-ui-globalOptions"] || {};
3405
- const resolvedTemplates = useMemo7(
4060
+ const layoutLabelPlacement = uiSchema["x-ui-layout"]?.labelPlacement;
4061
+ const globalOptions = useMemo8(
4062
+ () => ({
4063
+ ...layoutLabelPlacement ? { labelPlacement: layoutLabelPlacement } : {},
4064
+ ...uiSchema["x-ui-globalOptions"] || {}
4065
+ }),
4066
+ [layoutLabelPlacement, uiSchema]
4067
+ );
4068
+ const resolvedTemplates = useMemo8(
3406
4069
  () => ({ ...DEFAULT_TEMPLATES, ...templates }),
3407
4070
  [templates]
3408
4071
  );
3409
4072
  const nestedRenderer = useCallback8(
3410
- (props2) => /* @__PURE__ */ jsx28(
4073
+ (props2) => /* @__PURE__ */ jsx37(
3411
4074
  JsonSchemaFormRenderer,
3412
4075
  {
3413
4076
  ...props2,
@@ -3418,38 +4081,94 @@ function JsonSchemaFormRenderer(props) {
3418
4081
  ),
3419
4082
  [resolvedTemplates]
3420
4083
  );
3421
- const contextValue = useMemo7(
4084
+ const contextValue = useMemo8(
3422
4085
  () => ({ renderer: nestedRenderer, templates: resolvedTemplates }),
3423
4086
  [nestedRenderer, resolvedTemplates]
3424
4087
  );
3425
4088
  const needsBlurHandler = deferStateUpdates || !!onBlur;
3426
- const fields = orderedProperties.map(([fieldName, property]) => /* @__PURE__ */ jsx28(
3427
- FormFieldRenderer,
3428
- {
3429
- fieldName,
3430
- property,
3431
- value: displayData[fieldName],
3432
- onChange: (v) => handleFieldChange(fieldName, v),
3433
- onBlur: needsBlurHandler ? () => handleFieldBlur(fieldName) : void 0,
3434
- required: isFieldRequired(resolvedSchema, fieldName),
4089
+ const fields = useMemo8(
4090
+ () => orderedProperties.flatMap(([fieldName, property]) => {
4091
+ const rule = uiSchema[fieldName]?.["x-ui-rule"] ?? property["x-ui-rule"];
4092
+ const { visible, enabled } = evaluateUiRule(rule, displayData);
4093
+ if (!visible) return [];
4094
+ return [
4095
+ /* @__PURE__ */ jsx37(
4096
+ FormFieldRenderer,
4097
+ {
4098
+ fieldName,
4099
+ property,
4100
+ value: displayData[fieldName],
4101
+ onChange: (v) => handleFieldChange(fieldName, v),
4102
+ onBlur: needsBlurHandler ? () => handleFieldBlur(fieldName) : void 0,
4103
+ required: isFieldRequired(resolvedSchema, fieldName),
4104
+ disabled: disabled || !enabled,
4105
+ readOnly,
4106
+ schema,
4107
+ uiSchema: uiSchema[fieldName],
4108
+ registry: widgets,
4109
+ globalOptions,
4110
+ formData: displayData,
4111
+ idPrefix
4112
+ },
4113
+ fieldName
4114
+ )
4115
+ ];
4116
+ }),
4117
+ [
4118
+ orderedProperties,
4119
+ displayData,
4120
+ resolvedSchema,
3435
4121
  disabled,
3436
4122
  readOnly,
3437
4123
  schema,
3438
- uiSchema: uiSchema[fieldName],
3439
- registry: widgets,
4124
+ uiSchema,
4125
+ widgets,
3440
4126
  globalOptions,
3441
- formData: displayData
3442
- },
3443
- fieldName
3444
- ));
4127
+ idPrefix,
4128
+ needsBlurHandler,
4129
+ handleFieldChange,
4130
+ handleFieldBlur
4131
+ ]
4132
+ );
3445
4133
  const layoutConfig = uiSchema["x-ui-layout"];
3446
4134
  const rowDefs = layoutConfig?.rows;
4135
+ const groupDefs = layoutConfig?.groups;
3447
4136
  const defaultRowGap = layoutConfig?.gap ?? 24;
3448
- const layoutFields = useMemo7(
3449
- () => rowDefs && rowDefs.length > 0 ? layoutFieldsInRows(fields, rowDefs, defaultRowGap) : fields,
3450
- [fields, rowDefs, defaultRowGap]
4137
+ const groupTemplate = resolveGroupTemplate(
4138
+ resolvedTemplates,
4139
+ layoutConfig?.variant
3451
4140
  );
3452
- return /* @__PURE__ */ jsx28(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx28(
4141
+ const layoutFields = useMemo8(() => {
4142
+ if (groupDefs && groupDefs.length > 0) {
4143
+ return layoutFieldsInGroups(
4144
+ fields,
4145
+ groupDefs,
4146
+ defaultRowGap,
4147
+ fieldErrors,
4148
+ displayData,
4149
+ groupTemplate
4150
+ );
4151
+ }
4152
+ if (rowDefs && rowDefs.length > 0) {
4153
+ return layoutFieldsInRows(fields, rowDefs, defaultRowGap);
4154
+ }
4155
+ return fields;
4156
+ }, [
4157
+ fields,
4158
+ rowDefs,
4159
+ groupDefs,
4160
+ defaultRowGap,
4161
+ fieldErrors,
4162
+ displayData,
4163
+ groupTemplate
4164
+ ]);
4165
+ const hiddenErrorCount = useMemo8(() => {
4166
+ if (!groupDefs?.length || fieldErrors.length === 0) return 0;
4167
+ const hidden = hiddenGroupFieldNames(groupDefs, displayData);
4168
+ if (hidden.size === 0) return 0;
4169
+ return fieldErrors.filter((e) => hidden.has(pointerRoot(e.pointer))).length;
4170
+ }, [groupDefs, fieldErrors, displayData]);
4171
+ return /* @__PURE__ */ jsx37(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx37(
3453
4172
  FormContainer,
3454
4173
  {
3455
4174
  displayContents,
@@ -3460,19 +4179,20 @@ function JsonSchemaFormRenderer(props) {
3460
4179
  handleSave,
3461
4180
  handleCancel,
3462
4181
  disabled,
3463
- hasChanges,
4182
+ hasChanges: hasChanges || allowPristineSave,
3464
4183
  isValid,
3465
4184
  saveLabel,
3466
4185
  cancelLabel,
3467
4186
  showCancel,
3468
4187
  leadingActions,
4188
+ hiddenErrorCount,
3469
4189
  children: layoutFields
3470
4190
  }
3471
4191
  ) });
3472
4192
  }
3473
4193
 
3474
4194
  // src/hooks/useResolvedSchema.ts
3475
- import { useMemo as useMemo8 } from "react";
4195
+ import { useMemo as useMemo9 } from "react";
3476
4196
  import {
3477
4197
  getSchemaProperties as getSchemaProperties2,
3478
4198
  resolveSchema as resolveSchema2
@@ -3482,11 +4202,11 @@ function useResolvedSchema({
3482
4202
  formData,
3483
4203
  uiSchema
3484
4204
  }) {
3485
- const resolvedSchema = useMemo8(
4205
+ const resolvedSchema = useMemo9(
3486
4206
  () => resolveSchema2(schema, formData),
3487
4207
  [schema, formData]
3488
4208
  );
3489
- const orderedProperties = useMemo8(() => {
4209
+ const orderedProperties = useMemo9(() => {
3490
4210
  const properties = getSchemaProperties2(resolvedSchema);
3491
4211
  const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
3492
4212
  const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
@@ -3500,6 +4220,9 @@ export {
3500
4220
  DEFAULT_TEMPLATES,
3501
4221
  DEFAULT_WIDGETS,
3502
4222
  DEFAULT_WIDGET_MAP,
4223
+ DateTimeWidget,
4224
+ DateWidget,
4225
+ DisplayWidget,
3503
4226
  EntitySelectWidget,
3504
4227
  ExtraPropertiesCard_default as ExtraPropertiesCard,
3505
4228
  FormActions,
@@ -3513,18 +4236,25 @@ export {
3513
4236
  FormSelect,
3514
4237
  FormTagsInput,
3515
4238
  FormTextarea,
4239
+ GroupErrorBadge,
3516
4240
  JsonEditorWidget,
3517
4241
  JsonSchemaFormRenderer,
3518
4242
  NumberWidget,
3519
4243
  ObjectWidget,
4244
+ OneOfWidget,
3520
4245
  QuantityWidget,
4246
+ SectionsGroupTemplate,
3521
4247
  SelectWidget,
4248
+ StepperGroupTemplate,
4249
+ TabsGroupTemplate,
3522
4250
  TagsInputWidget,
3523
4251
  TextInputWidget,
3524
4252
  TextareaWidget,
4253
+ TimeWidget,
3525
4254
  firstError,
3526
4255
  getAriaDescribedBy,
3527
4256
  getDefaultWidgetForType,
4257
+ groupTemplateKey,
3528
4258
  resolveWidget,
3529
4259
  useFormEngine,
3530
4260
  useNestedFormContext,