@petrarca/sonnet-forms 0.5.0 → 0.7.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
@@ -1483,16 +1483,18 @@ function FormActions({
1483
1483
  disabled = false,
1484
1484
  hasChanges,
1485
1485
  isValid,
1486
+ saveWhenInvalid = "reveal",
1486
1487
  saveLabel = "Save",
1487
1488
  cancelLabel = "Cancel",
1488
1489
  showCancel = true,
1489
1490
  leadingActions
1490
1491
  }) {
1492
+ const saveDisabled = disabled || !hasChanges || saveWhenInvalid === "disable" && !isValid;
1491
1493
  return /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between mt-4", children: [
1492
1494
  /* @__PURE__ */ jsx10("div", { className: "flex items-center gap-2", children: leadingActions }),
1493
1495
  /* @__PURE__ */ jsxs9(SimpleGroup, { justify: "flex-end", children: [
1494
1496
  showCancel && /* @__PURE__ */ jsx10(Button3, { variant: "outline", onClick: onCancel, disabled, children: cancelLabel }),
1495
- /* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: disabled || !hasChanges || !isValid, children: saveLabel })
1497
+ /* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: saveDisabled, children: saveLabel })
1496
1498
  ] })
1497
1499
  ] });
1498
1500
  }
@@ -1533,12 +1535,13 @@ function ExtraPropertiesCard({ properties, schema }) {
1533
1535
  var ExtraPropertiesCard_default = ExtraPropertiesCard;
1534
1536
 
1535
1537
  // src/JsonSchemaFormRenderer.tsx
1536
- import React11, { useCallback as useCallback8, useMemo as useMemo7 } from "react";
1538
+ import React13, { useCallback as useCallback8, useContext as useContext2, useMemo as useMemo8 } from "react";
1537
1539
  import { SimpleStack } from "@petrarca/sonnet-ui";
1540
+ import { AlertCircle as AlertCircle2 } from "lucide-react";
1538
1541
  import {
1539
1542
  WIDTH_TOKEN_REMS
1540
1543
  } from "@petrarca/sonnet-core/schema";
1541
- import { isFieldRequired } from "@petrarca/sonnet-core/schema";
1544
+ import { isFieldRequired, evaluateUiRule } from "@petrarca/sonnet-core/schema";
1542
1545
  import { FORM_ITEM_ID_FIELD as FORM_ITEM_ID_FIELD2 } from "@petrarca/sonnet-core";
1543
1546
 
1544
1547
  // src/widgets/types.ts
@@ -1794,6 +1797,7 @@ function SelectWidget(props) {
1794
1797
  const enumNames = widgetOptions.enumNames || {};
1795
1798
  const clearable = widgetOptions.clearable !== false && !required;
1796
1799
  const isMultiple = widgetOptions.multiple ?? propertySchema.type === "array";
1800
+ const creatable = !!widgetOptions.creatable;
1797
1801
  const selectOptions = buildSelectOptions(propertySchema, enumNames);
1798
1802
  const error = firstError(rawErrors);
1799
1803
  const compact = widgetOptions?.compact;
@@ -1844,7 +1848,9 @@ function SelectWidget(props) {
1844
1848
  autoFocus: autofocus,
1845
1849
  onBlur: handleBlur,
1846
1850
  className: triggerClassName,
1847
- compact
1851
+ compact,
1852
+ allowCreate: creatable,
1853
+ onCreate: (label2) => onChange(label2)
1848
1854
  }
1849
1855
  );
1850
1856
  }
@@ -2190,9 +2196,11 @@ function resolveLayout(uiSchema, propertySchema) {
2190
2196
  return uiSchema?.["x-ui-layout"] ?? propertySchema["x-ui-layout"];
2191
2197
  }
2192
2198
  function ObjectWidget({
2199
+ id,
2193
2200
  propertySchema,
2194
2201
  value,
2195
2202
  onChange,
2203
+ onBlur,
2196
2204
  registry,
2197
2205
  uiSchema,
2198
2206
  disabled,
@@ -2229,7 +2237,9 @@ function ObjectWidget({
2229
2237
  readOnly: readonly,
2230
2238
  widgets: registry,
2231
2239
  uiSchema: effectiveUiSchema,
2232
- displayContents
2240
+ displayContents,
2241
+ idPrefix: id,
2242
+ onBlur: onBlur ? () => onBlur(id, value) : void 0
2233
2243
  }
2234
2244
  );
2235
2245
  if (displayContents) return /* @__PURE__ */ jsx21(Fragment3, { children: content });
@@ -2696,9 +2706,310 @@ function QuantityWidget({
2696
2706
  );
2697
2707
  }
2698
2708
 
2709
+ // src/widgets/DateWidget.tsx
2710
+ import { DateInput } from "@petrarca/sonnet-ui";
2711
+ import { jsx as jsx26 } from "react/jsx-runtime";
2712
+ function resolvePrecision(value) {
2713
+ return value === "year" || value === "month" ? value : "day";
2714
+ }
2715
+ function DateWidget({
2716
+ id,
2717
+ value,
2718
+ onChange,
2719
+ onBlur,
2720
+ required,
2721
+ disabled,
2722
+ rawErrors,
2723
+ label,
2724
+ description,
2725
+ placeholder,
2726
+ options
2727
+ }) {
2728
+ const error = firstError(rawErrors);
2729
+ const precision = resolvePrecision(options?.precision);
2730
+ const locale = typeof options?.locale === "string" ? options.locale : void 0;
2731
+ return /* @__PURE__ */ jsx26(
2732
+ FormFieldWrapper,
2733
+ {
2734
+ inputId: id,
2735
+ label,
2736
+ description,
2737
+ required,
2738
+ labelPlacement: options?.labelPlacement,
2739
+ children: /* @__PURE__ */ jsx26(
2740
+ DateInput,
2741
+ {
2742
+ id,
2743
+ value: typeof value === "string" ? value : "",
2744
+ onChange,
2745
+ onBlur: () => onBlur?.(id, value),
2746
+ precision,
2747
+ locale,
2748
+ placeholder,
2749
+ required,
2750
+ disabled,
2751
+ error
2752
+ }
2753
+ )
2754
+ }
2755
+ );
2756
+ }
2757
+
2758
+ // src/widgets/TimeWidget.tsx
2759
+ import { TimeInput } from "@petrarca/sonnet-ui";
2760
+ import { jsx as jsx27 } from "react/jsx-runtime";
2761
+ function resolveGranularity(value) {
2762
+ return value === "second" ? "second" : "minute";
2763
+ }
2764
+ function TimeWidget({
2765
+ id,
2766
+ value,
2767
+ onChange,
2768
+ onBlur,
2769
+ required,
2770
+ disabled,
2771
+ rawErrors,
2772
+ label,
2773
+ description,
2774
+ placeholder,
2775
+ options
2776
+ }) {
2777
+ const error = firstError(rawErrors);
2778
+ const granularity = resolveGranularity(options?.granularity);
2779
+ return /* @__PURE__ */ jsx27(
2780
+ FormFieldWrapper,
2781
+ {
2782
+ inputId: id,
2783
+ label,
2784
+ description,
2785
+ required,
2786
+ labelPlacement: options?.labelPlacement,
2787
+ children: /* @__PURE__ */ jsx27(
2788
+ TimeInput,
2789
+ {
2790
+ id,
2791
+ value: typeof value === "string" ? value : "",
2792
+ onChange,
2793
+ onBlur: () => onBlur?.(id, value),
2794
+ granularity,
2795
+ placeholder,
2796
+ required,
2797
+ disabled,
2798
+ error
2799
+ }
2800
+ )
2801
+ }
2802
+ );
2803
+ }
2804
+
2805
+ // src/widgets/DateTimeWidget.tsx
2806
+ import { DateTimeInput } from "@petrarca/sonnet-ui";
2807
+ import { jsx as jsx28 } from "react/jsx-runtime";
2808
+ function resolveGranularity2(value) {
2809
+ return value === "second" ? "second" : "minute";
2810
+ }
2811
+ function DateTimeWidget({
2812
+ id,
2813
+ value,
2814
+ onChange,
2815
+ onBlur,
2816
+ required,
2817
+ disabled,
2818
+ rawErrors,
2819
+ label,
2820
+ description,
2821
+ options
2822
+ }) {
2823
+ const error = firstError(rawErrors);
2824
+ const granularity = resolveGranularity2(options?.granularity);
2825
+ const timeZone = typeof options?.timeZone === "string" ? options.timeZone : void 0;
2826
+ const locale = typeof options?.locale === "string" ? options.locale : void 0;
2827
+ return /* @__PURE__ */ jsx28(
2828
+ FormFieldWrapper,
2829
+ {
2830
+ inputId: id,
2831
+ label,
2832
+ description,
2833
+ required,
2834
+ labelPlacement: options?.labelPlacement,
2835
+ children: /* @__PURE__ */ jsx28(
2836
+ DateTimeInput,
2837
+ {
2838
+ idPrefix: id,
2839
+ value: typeof value === "string" ? value : "",
2840
+ onChange,
2841
+ onBlur: () => onBlur?.(id, value),
2842
+ granularity,
2843
+ timeZone,
2844
+ locale,
2845
+ required,
2846
+ disabled,
2847
+ error
2848
+ }
2849
+ )
2850
+ }
2851
+ );
2852
+ }
2853
+
2854
+ // src/widgets/DisplayWidget.tsx
2855
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
2856
+ function DisplayWidget({
2857
+ label,
2858
+ description
2859
+ }) {
2860
+ if (!label && !description) return null;
2861
+ return /* @__PURE__ */ jsxs17("div", { className: "space-y-1", children: [
2862
+ label && /* @__PURE__ */ jsx29("p", { className: "text-sm text-foreground", children: label }),
2863
+ description && /* @__PURE__ */ jsx29("p", { className: "text-sm text-muted-foreground", children: description })
2864
+ ] });
2865
+ }
2866
+
2867
+ // src/widgets/OneOfWidget.tsx
2868
+ import { useMemo as useMemo6 } from "react";
2869
+ import {
2870
+ getDiscriminator,
2871
+ getBranchValue,
2872
+ getBranchLabel,
2873
+ findActiveBranch
2874
+ } from "@petrarca/sonnet-core/schema";
2875
+ import {
2876
+ Label,
2877
+ Select,
2878
+ SelectTrigger,
2879
+ SelectValue,
2880
+ SelectContent,
2881
+ SelectItem
2882
+ } from "@petrarca/sonnet-ui";
2883
+ import { jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
2884
+ function buildChoices(branches, discriminator) {
2885
+ return branches.map((branch, index) => {
2886
+ const raw = discriminator ? getBranchValue(branch, discriminator) : void 0;
2887
+ return {
2888
+ index,
2889
+ value: raw !== void 0 ? String(raw) : String(index),
2890
+ label: getBranchLabel(branch, discriminator, index)
2891
+ };
2892
+ });
2893
+ }
2894
+ function buildBranchSchema(branch) {
2895
+ if (!branch.properties) return null;
2896
+ return {
2897
+ title: branch.title || "",
2898
+ type: "object",
2899
+ properties: branch.properties,
2900
+ required: branch.required,
2901
+ ...branch["x-ui-order"] ? { "x-ui-order": branch["x-ui-order"] } : {}
2902
+ };
2903
+ }
2904
+ function branchChangeData(choices, branches, discriminator, selected) {
2905
+ const choice = choices.find((c) => c.value === selected);
2906
+ if (!choice || !discriminator) return {};
2907
+ const discValue = getBranchValue(branches[choice.index], discriminator) ?? selected;
2908
+ return { [discriminator]: discValue };
2909
+ }
2910
+ function BranchSelector({
2911
+ id,
2912
+ label,
2913
+ description,
2914
+ choices,
2915
+ activeValue,
2916
+ disabled,
2917
+ onSelect
2918
+ }) {
2919
+ return /* @__PURE__ */ jsxs18("div", { className: "space-y-1", children: [
2920
+ label && /* @__PURE__ */ jsx30(Label, { htmlFor: id, children: label }),
2921
+ /* @__PURE__ */ jsxs18(Select, { value: activeValue, onValueChange: onSelect, disabled, children: [
2922
+ /* @__PURE__ */ jsx30(SelectTrigger, { id, children: /* @__PURE__ */ jsx30(SelectValue, { placeholder: "Select an option" }) }),
2923
+ /* @__PURE__ */ jsx30(SelectContent, { children: choices.map((c) => /* @__PURE__ */ jsx30(SelectItem, { value: c.value, children: c.label }, c.value)) })
2924
+ ] }),
2925
+ description && /* @__PURE__ */ jsx30("p", { className: "text-sm text-muted-foreground", children: description })
2926
+ ] });
2927
+ }
2928
+ function resolveActive(propertySchema, branches, choices, discriminator, data) {
2929
+ const index = findActiveBranch(propertySchema, data, discriminator);
2930
+ if (index < 0) return { value: "", label: "", subSchema: null };
2931
+ return {
2932
+ value: choices[index]?.value ?? "",
2933
+ label: choices[index]?.label ?? "",
2934
+ subSchema: buildBranchSchema(branches[index])
2935
+ };
2936
+ }
2937
+ function OneOfWidget({
2938
+ propertySchema,
2939
+ value,
2940
+ onChange,
2941
+ registry,
2942
+ uiSchema,
2943
+ disabled,
2944
+ readonly,
2945
+ label,
2946
+ description,
2947
+ id,
2948
+ options
2949
+ }) {
2950
+ const { renderer: Renderer, templates } = useNestedFormContext();
2951
+ const ContainerTemplate = templates.ObjectContainerTemplate ?? DefaultObjectContainerTemplate;
2952
+ const branches = useMemo6(() => propertySchema.oneOf ?? [], [propertySchema]);
2953
+ const discriminator = useMemo6(
2954
+ () => getDiscriminator(propertySchema),
2955
+ [propertySchema]
2956
+ );
2957
+ const choices = useMemo6(
2958
+ () => buildChoices(branches, discriminator),
2959
+ [branches, discriminator]
2960
+ );
2961
+ const data = value || {};
2962
+ const active = resolveActive(
2963
+ propertySchema,
2964
+ branches,
2965
+ choices,
2966
+ discriminator,
2967
+ data
2968
+ );
2969
+ if (branches.length === 0) return null;
2970
+ const subUiSchema = uiSchema?.["x-ui-fields"] ?? {};
2971
+ return /* @__PURE__ */ jsxs18("div", { className: "space-y-4", children: [
2972
+ /* @__PURE__ */ jsx30(
2973
+ BranchSelector,
2974
+ {
2975
+ id,
2976
+ label,
2977
+ description,
2978
+ choices,
2979
+ activeValue: active.value,
2980
+ disabled: disabled || readonly,
2981
+ onSelect: (selected) => onChange(branchChangeData(choices, branches, discriminator, selected))
2982
+ }
2983
+ ),
2984
+ active.subSchema && /* @__PURE__ */ jsx30(
2985
+ ContainerTemplate,
2986
+ {
2987
+ label: active.label,
2988
+ description: void 0,
2989
+ variant: options?.variant || "section",
2990
+ defaultOpen: true,
2991
+ children: /* @__PURE__ */ jsx30(
2992
+ Renderer,
2993
+ {
2994
+ schema: active.subSchema,
2995
+ data,
2996
+ onChange,
2997
+ disabled,
2998
+ readOnly: readonly,
2999
+ widgets: registry,
3000
+ uiSchema: subUiSchema,
3001
+ idPrefix: id
3002
+ }
3003
+ )
3004
+ }
3005
+ )
3006
+ ] });
3007
+ }
3008
+
2699
3009
  // src/widgets/widgetResolver.ts
2700
3010
  import {
2701
3011
  isNullableBoolean as isNullableBoolean2,
3012
+ isOneOf,
2702
3013
  extractEnumValues as extractEnumValues2,
2703
3014
  getFieldType
2704
3015
  } from "@petrarca/sonnet-core/schema";
@@ -2710,8 +3021,11 @@ var DEFAULT_WIDGET_MAP = {
2710
3021
  // Uses email input type
2711
3022
  uri: "TextInputWidget",
2712
3023
  // Uses url input type
2713
- url: "TextInputWidget"
3024
+ url: "TextInputWidget",
2714
3025
  // Uses url input type
3026
+ date: "DateWidget",
3027
+ "date-time": "DateTimeWidget",
3028
+ time: "TimeWidget"
2715
3029
  },
2716
3030
  number: {
2717
3031
  default: "NumberWidget"
@@ -2722,6 +3036,9 @@ var DEFAULT_WIDGET_MAP = {
2722
3036
  },
2723
3037
  boolean: {
2724
3038
  default: "CheckboxWidget"
3039
+ },
3040
+ null: {
3041
+ default: "DisplayWidget"
2725
3042
  }
2726
3043
  };
2727
3044
  function resolveWidgetByName(widgetName, propertySchema, registry) {
@@ -2771,6 +3088,7 @@ function registryLookup(registry, widgetName) {
2771
3088
  return registry[widgetName] || registry["TextInputWidget"];
2772
3089
  }
2773
3090
  function resolveFromSchemaStructure(schema, registry) {
3091
+ if (isOneOf(schema)) return registryLookup(registry, "OneOfWidget");
2774
3092
  if (isNullableBoolean2(schema))
2775
3093
  return registryLookup(registry, "CheckboxWidget");
2776
3094
  if (extractEnumValues2(schema).length > 0)
@@ -2814,6 +3132,11 @@ var DEFAULT_WIDGETS = {
2814
3132
  ObjectWidget,
2815
3133
  ArrayWidget,
2816
3134
  QuantityWidget,
3135
+ DateWidget,
3136
+ TimeWidget,
3137
+ DateTimeWidget,
3138
+ DisplayWidget,
3139
+ OneOfWidget,
2817
3140
  // Lowercase aliases for x-ui-widget convenience
2818
3141
  text: TextInputWidget,
2819
3142
  textarea: TextareaWidget,
@@ -2825,13 +3148,18 @@ var DEFAULT_WIDGETS = {
2825
3148
  "entity-select": EntitySelectWidget,
2826
3149
  object: ObjectWidget,
2827
3150
  array: ArrayWidget,
2828
- quantity: QuantityWidget
3151
+ quantity: QuantityWidget,
3152
+ date: DateWidget,
3153
+ time: TimeWidget,
3154
+ "date-time": DateTimeWidget,
3155
+ display: DisplayWidget,
3156
+ "one-of": OneOfWidget
2829
3157
  };
2830
3158
 
2831
3159
  // src/templates/HorizontalArrayItemTemplate.tsx
2832
3160
  import { Trash2 as Trash22, ChevronUp as ChevronUp2, ChevronDown as ChevronDown4 } from "lucide-react";
2833
3161
  import { Button as Button6 } from "@petrarca/sonnet-ui";
2834
- import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
3162
+ import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
2835
3163
  function HorizontalArrayItemTemplate({
2836
3164
  children,
2837
3165
  index,
@@ -2855,7 +3183,7 @@ function HorizontalArrayItemTemplate({
2855
3183
  );
2856
3184
  const gap = resolvedLayout.gap ?? 8;
2857
3185
  const alignItems = resolvedLayout.compact ? "center" : "start";
2858
- return /* @__PURE__ */ jsxs17(
3186
+ return /* @__PURE__ */ jsxs19(
2859
3187
  "div",
2860
3188
  {
2861
3189
  "data-fid": fid,
@@ -2867,8 +3195,8 @@ function HorizontalArrayItemTemplate({
2867
3195
  },
2868
3196
  children: [
2869
3197
  children,
2870
- orderable && /* @__PURE__ */ jsxs17("div", { className: "flex flex-col items-center justify-center", children: [
2871
- /* @__PURE__ */ jsx26(
3198
+ orderable && /* @__PURE__ */ jsxs19("div", { className: "flex flex-col items-center justify-center", children: [
3199
+ /* @__PURE__ */ jsx31(
2872
3200
  Button6,
2873
3201
  {
2874
3202
  type: "button",
@@ -2878,10 +3206,10 @@ function HorizontalArrayItemTemplate({
2878
3206
  onClick: onMoveUp,
2879
3207
  title: "Move up",
2880
3208
  className: "h-4 w-8 p-0",
2881
- children: /* @__PURE__ */ jsx26(ChevronUp2, { className: "h-3 w-3" })
3209
+ children: /* @__PURE__ */ jsx31(ChevronUp2, { className: "h-3 w-3" })
2882
3210
  }
2883
3211
  ),
2884
- /* @__PURE__ */ jsx26(
3212
+ /* @__PURE__ */ jsx31(
2885
3213
  Button6,
2886
3214
  {
2887
3215
  type: "button",
@@ -2891,11 +3219,11 @@ function HorizontalArrayItemTemplate({
2891
3219
  onClick: onMoveDown,
2892
3220
  title: "Move down",
2893
3221
  className: "h-4 w-8 p-0",
2894
- children: /* @__PURE__ */ jsx26(ChevronDown4, { className: "h-3 w-3" })
3222
+ children: /* @__PURE__ */ jsx31(ChevronDown4, { className: "h-3 w-3" })
2895
3223
  }
2896
3224
  )
2897
3225
  ] }),
2898
- canRemove && /* @__PURE__ */ jsx26(
3226
+ canRemove && /* @__PURE__ */ jsx31(
2899
3227
  Button6,
2900
3228
  {
2901
3229
  type: "button",
@@ -2905,7 +3233,7 @@ function HorizontalArrayItemTemplate({
2905
3233
  onClick: onRemove,
2906
3234
  title: "Remove item",
2907
3235
  className: "text-muted-foreground hover:text-destructive h-8 w-8 p-0",
2908
- children: /* @__PURE__ */ jsx26(Trash22, { className: "h-3.5 w-3.5" })
3236
+ children: /* @__PURE__ */ jsx31(Trash22, { className: "h-3.5 w-3.5" })
2909
3237
  }
2910
3238
  )
2911
3239
  ]
@@ -2913,13 +3241,123 @@ function HorizontalArrayItemTemplate({
2913
3241
  );
2914
3242
  }
2915
3243
 
3244
+ // src/templates/TabsGroupTemplate.tsx
3245
+ import { useState as useState7 } from "react";
3246
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@petrarca/sonnet-ui";
3247
+
3248
+ // src/templates/GroupErrorBadge.tsx
3249
+ import { AlertCircle } from "lucide-react";
3250
+ import { jsx as jsx32 } from "react/jsx-runtime";
3251
+ function GroupErrorBadge({
3252
+ count
3253
+ }) {
3254
+ if (count === 0) return null;
3255
+ const label = count === 1 ? "1 field needs attention" : `${count} fields need attention`;
3256
+ return /* @__PURE__ */ jsx32(
3257
+ "span",
3258
+ {
3259
+ role: "img",
3260
+ "aria-label": label,
3261
+ title: label,
3262
+ className: "inline-flex shrink-0 text-destructive",
3263
+ children: /* @__PURE__ */ jsx32(AlertCircle, { className: "h-3.5 w-3.5" })
3264
+ }
3265
+ );
3266
+ }
3267
+
3268
+ // src/templates/TabsGroupTemplate.tsx
3269
+ import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
3270
+ function TabsGroupTemplate({
3271
+ groups
3272
+ }) {
3273
+ const [active, setActive] = useState7(groups[0].key);
3274
+ const value = groups.some((g) => g.key === active) ? active : groups[0].key;
3275
+ return /* @__PURE__ */ jsxs20(Tabs, { value, onValueChange: setActive, children: [
3276
+ /* @__PURE__ */ jsx33(TabsList, { className: "mb-4", children: groups.map((g) => /* @__PURE__ */ jsxs20(TabsTrigger, { value: g.key, className: "gap-1.5", children: [
3277
+ g.label,
3278
+ /* @__PURE__ */ jsx33(GroupErrorBadge, { count: g.errorCount })
3279
+ ] }, g.key)) }),
3280
+ groups.map((g) => /* @__PURE__ */ jsx33(TabsContent, { value: g.key, className: "space-y-4", children: g.content }, g.key))
3281
+ ] });
3282
+ }
3283
+
3284
+ // src/templates/StepperGroupTemplate.tsx
3285
+ import { useState as useState8 } from "react";
3286
+ import {
3287
+ Button as Button7,
3288
+ Stepper,
3289
+ StepperList,
3290
+ StepperItem,
3291
+ StepperTrigger,
3292
+ StepperIndicator,
3293
+ StepperSeparator,
3294
+ StepperTitle,
3295
+ StepperContent,
3296
+ StepperPrev,
3297
+ StepperNext
3298
+ } from "@petrarca/sonnet-ui";
3299
+ import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
3300
+ function StepperGroupTemplate({
3301
+ groups
3302
+ }) {
3303
+ const [active, setActive] = useState8(groups[0].key);
3304
+ const value = groups.some((g) => g.key === active) ? active : groups[0].key;
3305
+ const isLast = value === groups[groups.length - 1].key;
3306
+ return /* @__PURE__ */ jsxs21(Stepper, { value, onValueChange: setActive, children: [
3307
+ /* @__PURE__ */ jsx34(StepperList, { className: "mb-4 gap-2", children: groups.map((g) => /* @__PURE__ */ jsxs21(StepperItem, { value: g.key, children: [
3308
+ /* @__PURE__ */ jsxs21(StepperTrigger, { children: [
3309
+ /* @__PURE__ */ jsx34(StepperIndicator, {}),
3310
+ /* @__PURE__ */ jsxs21(StepperTitle, { className: "inline-flex items-center gap-1.5", children: [
3311
+ g.label,
3312
+ /* @__PURE__ */ jsx34(GroupErrorBadge, { count: g.errorCount })
3313
+ ] })
3314
+ ] }),
3315
+ /* @__PURE__ */ jsx34(StepperSeparator, {})
3316
+ ] }, g.key)) }),
3317
+ groups.map((g) => /* @__PURE__ */ jsx34(StepperContent, { value: g.key, className: "space-y-4", children: g.content }, g.key)),
3318
+ /* @__PURE__ */ jsxs21("div", { className: "mt-4 flex justify-between", children: [
3319
+ /* @__PURE__ */ jsx34(StepperPrev, { asChild: true, children: /* @__PURE__ */ jsx34(Button7, { variant: "outline", size: "sm", children: "Back" }) }),
3320
+ !isLast && /* @__PURE__ */ jsx34(StepperNext, { asChild: true, children: /* @__PURE__ */ jsx34(Button7, { size: "sm", children: "Next" }) })
3321
+ ] })
3322
+ ] });
3323
+ }
3324
+
3325
+ // src/templates/SectionsGroupTemplate.tsx
3326
+ import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
3327
+ function SectionsGroupTemplate({
3328
+ groups
3329
+ }) {
3330
+ return /* @__PURE__ */ jsx35("div", { className: "space-y-6", children: groups.map((g) => /* @__PURE__ */ jsxs22(
3331
+ "fieldset",
3332
+ {
3333
+ className: "rounded-md border border-border/60 px-3 pb-3 pt-1",
3334
+ children: [
3335
+ /* @__PURE__ */ jsxs22("legend", { className: "inline-flex items-center gap-1.5 px-1 text-sm font-medium text-muted-foreground", children: [
3336
+ g.label,
3337
+ /* @__PURE__ */ jsx35(GroupErrorBadge, { count: g.errorCount })
3338
+ ] }),
3339
+ /* @__PURE__ */ jsx35("div", { className: "space-y-4", children: g.content })
3340
+ ]
3341
+ },
3342
+ g.key
3343
+ )) });
3344
+ }
3345
+
2916
3346
  // src/templates/index.ts
2917
3347
  var DEFAULT_TEMPLATES = {
2918
3348
  ArrayItemTemplate: DefaultArrayItemTemplate,
2919
3349
  HorizontalArrayItemTemplate,
2920
3350
  ArrayHeaderTemplate: DefaultArrayHeaderTemplate,
2921
- ObjectContainerTemplate: DefaultObjectContainerTemplate
3351
+ ObjectContainerTemplate: DefaultObjectContainerTemplate,
3352
+ TabsGroupTemplate,
3353
+ StepperGroupTemplate,
3354
+ SectionsGroupTemplate
2922
3355
  };
3356
+ function groupTemplateKey(variant) {
3357
+ if (!variant) return "TabsGroupTemplate";
3358
+ const name = variant.charAt(0).toUpperCase() + variant.slice(1);
3359
+ return `${name}GroupTemplate`;
3360
+ }
2923
3361
 
2924
3362
  // src/FormFieldRenderer.tsx
2925
3363
  import { warnLog } from "@petrarca/sonnet-core";
@@ -2928,7 +3366,7 @@ import {
2928
3366
  getFieldHelpText,
2929
3367
  getFieldPlaceholder
2930
3368
  } from "@petrarca/sonnet-core/schema";
2931
- import { jsx as jsx27 } from "react/jsx-runtime";
3369
+ import { jsx as jsx36 } from "react/jsx-runtime";
2932
3370
  function mergeFieldOptions(globalOptions, property, uiSchema) {
2933
3371
  const xWidgetOptions = property["x-ui-options"] || {};
2934
3372
  const fieldOptions = uiSchema?.["x-ui-options"] || {};
@@ -2955,7 +3393,7 @@ function applyFieldWidth(widget, uiSchema, mergedOptions) {
2955
3393
  }
2956
3394
  const width = candidate && candidate in FIELD_WIDTH_CLASS ? candidate : "full";
2957
3395
  if (width === "full") return widget;
2958
- return /* @__PURE__ */ jsx27("div", { className: FIELD_WIDTH_CLASS[width], children: widget });
3396
+ return /* @__PURE__ */ jsx36("div", { className: FIELD_WIDTH_CLASS[width], children: widget });
2959
3397
  }
2960
3398
  function resolvePresentationText(property, uiSchema) {
2961
3399
  return {
@@ -2983,7 +3421,8 @@ function FormFieldRenderer({
2983
3421
  registry,
2984
3422
  globalOptions = {},
2985
3423
  formData,
2986
- idPrefix = "field"
3424
+ idPrefix = "field",
3425
+ rawErrors
2987
3426
  }) {
2988
3427
  const label = getFieldTitle(fieldName, property);
2989
3428
  const { description, placeholder } = resolvePresentationText(
@@ -3005,7 +3444,7 @@ function FormFieldRenderer({
3005
3444
  uiSchema?.["x-ui-title"],
3006
3445
  globalOptions.label
3007
3446
  );
3008
- const widget = /* @__PURE__ */ jsx27(
3447
+ const widget = /* @__PURE__ */ jsx36(
3009
3448
  Widget,
3010
3449
  {
3011
3450
  id: `${idPrefix}-${fieldName}`,
@@ -3024,14 +3463,15 @@ function FormFieldRenderer({
3024
3463
  disabled: flags.disabled,
3025
3464
  readonly: flags.readOnly,
3026
3465
  options: mergedOptions,
3027
- formData
3466
+ formData,
3467
+ rawErrors
3028
3468
  }
3029
3469
  );
3030
3470
  return applyFieldWidth(widget, uiSchema, mergedOptions);
3031
3471
  }
3032
3472
 
3033
3473
  // src/hooks/useFormEngine.ts
3034
- import { useState as useState7, useEffect as useEffect3, useMemo as useMemo6, useCallback as useCallback7, useRef as useRef5 } from "react";
3474
+ import { useState as useState9, useEffect as useEffect3, useMemo as useMemo7, useCallback as useCallback7, useRef as useRef5 } from "react";
3035
3475
  import { isEqual } from "@petrarca/sonnet-core";
3036
3476
  import {
3037
3477
  getSchemaProperties,
@@ -3040,7 +3480,8 @@ import {
3040
3480
  resolveSchema,
3041
3481
  resolveRefs,
3042
3482
  validateFormData,
3043
- diffFormData
3483
+ diffFormData,
3484
+ ensureFormItemIds
3044
3485
  } from "@petrarca/sonnet-core/schema";
3045
3486
 
3046
3487
  // src/hooks/orderProperties.ts
@@ -3099,14 +3540,24 @@ function useFormEngine({
3099
3540
  showActions = false,
3100
3541
  deferChanges = false,
3101
3542
  deferStateUpdates = false,
3102
- allowPristineSave = false
3543
+ allowPristineSave = false,
3544
+ refResolver
3103
3545
  }) {
3104
- const refResolvedSchema = useMemo6(() => resolveRefs(schema), [schema]);
3105
- const [originalData, setOriginalData] = useState7(
3106
- () => applySchemaDefaults2(data, refResolvedSchema)
3546
+ const refResolvedSchema = useMemo7(
3547
+ () => resolveRefs(schema, refResolver),
3548
+ [schema, refResolver]
3107
3549
  );
3108
- const [originalDataInitialized, setOriginalDataInitialized] = useState7(false);
3109
- const [nonSchemaData, setNonSchemaData] = useState7(
3550
+ const seed = useState9(
3551
+ () => ensureFormItemIds(
3552
+ applySchemaDefaults2(data, refResolvedSchema),
3553
+ refResolvedSchema
3554
+ )
3555
+ )[0];
3556
+ const [originalData, setOriginalData] = useState9(
3557
+ () => seed
3558
+ );
3559
+ const [originalDataInitialized, setOriginalDataInitialized] = useState9(false);
3560
+ const [nonSchemaData, setNonSchemaData] = useState9(
3110
3561
  () => {
3111
3562
  const props = getSchemaProperties(refResolvedSchema);
3112
3563
  const names = Object.keys(props);
@@ -3115,26 +3566,24 @@ function useFormEngine({
3115
3566
  );
3116
3567
  }
3117
3568
  );
3118
- const [formData, setFormData] = useState7(
3119
- () => applySchemaDefaults2(data, refResolvedSchema)
3120
- );
3121
- const [pendingChanges, setPendingChanges] = useState7({});
3122
- const resolvedSchema = useMemo6(
3569
+ const [formData, setFormData] = useState9(() => seed);
3570
+ const formDataRef = useRef5(formData);
3571
+ formDataRef.current = formData;
3572
+ const [pendingChanges, setPendingChanges] = useState9({});
3573
+ const resolvedSchema = useMemo7(
3123
3574
  () => resolveSchema(refResolvedSchema, formData),
3124
3575
  [refResolvedSchema, formData]
3125
3576
  );
3126
- const orderedProperties = useMemo6(() => {
3577
+ const orderedProperties = useMemo7(() => {
3127
3578
  const properties2 = getSchemaProperties(resolvedSchema);
3128
3579
  const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
3129
3580
  const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
3130
3581
  return orderProperties(properties2, orderArray);
3131
3582
  }, [resolvedSchema, uiSchema]);
3132
- const [isValid, setIsValid] = useState7(() => {
3133
- const initResolved = resolveSchema(refResolvedSchema, data);
3134
- const validation = validateFormData(initResolved, data);
3135
- return validation.isValid;
3136
- });
3137
- const [initialDefaultsSent, setInitialDefaultsSent] = useState7(false);
3583
+ const [initialDefaultsSent, setInitialDefaultsSent] = useState9(false);
3584
+ const [edited, setEdited] = useState9({});
3585
+ const [touched, setTouched] = useState9({});
3586
+ const [submitAttempted, setSubmitAttempted] = useState9(false);
3138
3587
  const properties = getSchemaProperties(resolvedSchema);
3139
3588
  useEffect3(() => {
3140
3589
  if (!initialDefaultsSent) {
@@ -3165,7 +3614,10 @@ function useFormEngine({
3165
3614
  useEffect3(() => {
3166
3615
  if (isEqual(prevDataRef.current, data)) return;
3167
3616
  prevDataRef.current = data;
3168
- const newData = applySchemaDefaults2(data, refResolvedSchema);
3617
+ const newData = ensureFormItemIds(
3618
+ applySchemaDefaults2(data, refResolvedSchema),
3619
+ refResolvedSchema
3620
+ );
3169
3621
  setFormData(newData);
3170
3622
  const schemaPropertyNames = Object.keys(properties);
3171
3623
  const result = computeBaselineUpdate(
@@ -3181,8 +3633,6 @@ function useFormEngine({
3181
3633
  setOriginalDataInitialized(true);
3182
3634
  }
3183
3635
  }
3184
- const validation = validateFormData(resolvedSchema, newData);
3185
- setIsValid(validation.isValid);
3186
3636
  }, [
3187
3637
  data,
3188
3638
  refResolvedSchema,
@@ -3191,21 +3641,34 @@ function useFormEngine({
3191
3641
  originalData,
3192
3642
  originalDataInitialized
3193
3643
  ]);
3194
- const hasChanges = useMemo6(
3644
+ const hasChanges = useMemo7(
3195
3645
  () => !isEqual(formData, originalData),
3196
3646
  [formData, originalData]
3197
3647
  );
3198
- const displayData = useMemo6(() => {
3648
+ const displayData = useMemo7(() => {
3199
3649
  if (deferStateUpdates && Object.keys(pendingChanges).length > 0) {
3200
3650
  return { ...formData, ...pendingChanges };
3201
3651
  }
3202
3652
  return formData;
3203
3653
  }, [formData, pendingChanges, deferStateUpdates]);
3654
+ const validation = useMemo7(
3655
+ () => validateFormData(resolvedSchema, displayData),
3656
+ [resolvedSchema, displayData]
3657
+ );
3658
+ const { isValid, fieldErrors } = validation;
3659
+ const onValidationChangeRef = useRef5(onValidationChange);
3660
+ onValidationChangeRef.current = onValidationChange;
3661
+ useEffect3(() => {
3662
+ onValidationChangeRef.current?.(validation.errors, validation.isValid);
3663
+ }, [validation]);
3204
3664
  const handleFieldChange = useCallback7(
3205
3665
  (fieldName, value) => {
3206
3666
  if (readOnly) return;
3667
+ setEdited(
3668
+ (prev) => prev[fieldName] ? prev : { ...prev, [fieldName]: true }
3669
+ );
3207
3670
  const immediate = isImmediateField(fieldName, properties);
3208
- if (showActions && deferStateUpdates && !immediate) {
3671
+ if (deferStateUpdates && !immediate) {
3209
3672
  setPendingChanges((prev) => ({ ...prev, [fieldName]: value }));
3210
3673
  return;
3211
3674
  }
@@ -3215,11 +3678,6 @@ function useFormEngine({
3215
3678
  const shouldCallOnChange = !deferChanges || immediate;
3216
3679
  if (shouldCallOnChange && onChange) onChange(newData);
3217
3680
  }
3218
- const validation = validateFormData(resolvedSchema, newData);
3219
- setIsValid(validation.isValid);
3220
- if (onValidationChange) {
3221
- onValidationChange(validation.errors, validation.isValid);
3222
- }
3223
3681
  },
3224
3682
  [
3225
3683
  readOnly,
@@ -3228,14 +3686,12 @@ function useFormEngine({
3228
3686
  deferStateUpdates,
3229
3687
  showActions,
3230
3688
  onChange,
3231
- resolvedSchema,
3232
- onValidationChange,
3233
3689
  properties
3234
3690
  ]
3235
3691
  );
3236
3692
  const flushPendingField = useCallback7(
3237
3693
  (fieldName) => {
3238
- if (!showActions || !deferStateUpdates) return;
3694
+ if (!deferStateUpdates) return;
3239
3695
  if (pendingChanges[fieldName] === void 0) return;
3240
3696
  const newData = { ...formData, ...pendingChanges };
3241
3697
  setFormData(newData);
@@ -3244,37 +3700,32 @@ function useFormEngine({
3244
3700
  delete updated[fieldName];
3245
3701
  return updated;
3246
3702
  });
3247
- const validation = validateFormData(resolvedSchema, newData);
3248
- setIsValid(validation.isValid);
3249
- if (onValidationChange) {
3250
- onValidationChange(validation.errors, validation.isValid);
3251
- }
3703
+ if (!showActions && onChange) onChange(newData);
3252
3704
  },
3253
- [
3254
- showActions,
3255
- deferStateUpdates,
3256
- pendingChanges,
3257
- formData,
3258
- resolvedSchema,
3259
- onValidationChange
3260
- ]
3705
+ [showActions, deferStateUpdates, pendingChanges, formData, onChange]
3261
3706
  );
3262
3707
  const handleFieldBlur = useCallback7(
3263
3708
  (fieldName) => {
3264
3709
  if (readOnly) return;
3710
+ if (edited[fieldName]) {
3711
+ setTouched(
3712
+ (prev) => prev[fieldName] ? prev : { ...prev, [fieldName]: true }
3713
+ );
3714
+ }
3265
3715
  flushPendingField(fieldName);
3716
+ const latest = formDataRef.current;
3266
3717
  if (!showActions && deferChanges && onChange) {
3267
- onChange(formData);
3718
+ onChange(latest);
3268
3719
  }
3269
3720
  if (onBlur) {
3270
3721
  const hasPending = deferStateUpdates && pendingChanges[fieldName] !== void 0;
3271
- const dataToPass = hasPending ? { ...formData, [fieldName]: pendingChanges[fieldName] } : formData;
3722
+ const dataToPass = hasPending ? { ...latest, [fieldName]: pendingChanges[fieldName] } : latest;
3272
3723
  onBlur(fieldName, dataToPass);
3273
3724
  }
3274
3725
  },
3275
3726
  [
3276
3727
  readOnly,
3277
- formData,
3728
+ edited,
3278
3729
  pendingChanges,
3279
3730
  deferChanges,
3280
3731
  deferStateUpdates,
@@ -3285,11 +3736,12 @@ function useFormEngine({
3285
3736
  ]
3286
3737
  );
3287
3738
  const handleSave = useCallback7(() => {
3739
+ setSubmitAttempted(true);
3288
3740
  if (!hasChanges && !allowPristineSave) return;
3289
3741
  const finalData = deferStateUpdates && Object.keys(pendingChanges).length > 0 ? { ...formData, ...pendingChanges } : formData;
3290
3742
  const completeData = { ...nonSchemaData, ...finalData };
3291
- const validation = validateFormData(resolvedSchema, finalData);
3292
- if (validation.isValid && onUpdate) {
3743
+ const validation2 = validateFormData(resolvedSchema, finalData);
3744
+ if (validation2.isValid && onUpdate) {
3293
3745
  const changed = {};
3294
3746
  for (const key of Object.keys(finalData)) {
3295
3747
  if (!isEqual(finalData[key], originalData[key])) {
@@ -3314,6 +3766,9 @@ function useFormEngine({
3314
3766
  const handleCancel = useCallback7(() => {
3315
3767
  setFormData({ ...originalData });
3316
3768
  setPendingChanges({});
3769
+ setEdited({});
3770
+ setTouched({});
3771
+ setSubmitAttempted(false);
3317
3772
  onCancel?.();
3318
3773
  }, [originalData, onCancel]);
3319
3774
  return {
@@ -3323,6 +3778,9 @@ function useFormEngine({
3323
3778
  orderedProperties,
3324
3779
  hasChanges,
3325
3780
  isValid,
3781
+ fieldErrors,
3782
+ touched,
3783
+ submitAttempted,
3326
3784
  handleFieldChange,
3327
3785
  handleFieldBlur,
3328
3786
  handleSave,
@@ -3331,7 +3789,11 @@ function useFormEngine({
3331
3789
  }
3332
3790
 
3333
3791
  // src/JsonSchemaFormRenderer.tsx
3334
- import { jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
3792
+ import {
3793
+ useValidationDisplay,
3794
+ useSaveWhenInvalid
3795
+ } from "@petrarca/sonnet-core/config";
3796
+ import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
3335
3797
  function stripFids(data) {
3336
3798
  const result = {};
3337
3799
  for (const [key, value] of Object.entries(data)) {
@@ -3398,7 +3860,7 @@ function useWrappedCallbacks(onChange, onBlur, onUpdate) {
3398
3860
  function buildFieldMap(fields) {
3399
3861
  const map = /* @__PURE__ */ new Map();
3400
3862
  for (const field of fields) {
3401
- if (React11.isValidElement(field) && field.key != null) {
3863
+ if (React13.isValidElement(field) && field.key != null) {
3402
3864
  map.set(String(field.key), field);
3403
3865
  }
3404
3866
  }
@@ -3417,12 +3879,12 @@ function resolveGridColumns(row, fieldCount) {
3417
3879
  }
3418
3880
  function wrapInGroup(group, key, content) {
3419
3881
  if (!group) return content;
3420
- return /* @__PURE__ */ jsxs18(
3882
+ return /* @__PURE__ */ jsxs23(
3421
3883
  "fieldset",
3422
3884
  {
3423
3885
  className: "rounded-md border border-border/60 px-3 pb-3 pt-1",
3424
3886
  children: [
3425
- /* @__PURE__ */ jsx28("legend", { className: "px-1 text-sm font-medium text-muted-foreground", children: group }),
3887
+ /* @__PURE__ */ jsx37("legend", { className: "px-1 text-sm font-medium text-muted-foreground", children: group }),
3426
3888
  content
3427
3889
  ]
3428
3890
  },
@@ -3444,7 +3906,7 @@ function buildRowElement(row, fieldMap, defaultRowGap) {
3444
3906
  gridTemplateColumns: resolveGridColumns(row, rowFields.length),
3445
3907
  gap: `${rowGap}px`
3446
3908
  };
3447
- const inner = /* @__PURE__ */ jsx28("div", { style, children: rowFields }, `row:${rowKey}`);
3909
+ const inner = /* @__PURE__ */ jsx37("div", { style, children: rowFields }, `row:${rowKey}`);
3448
3910
  return {
3449
3911
  element: wrapInGroup(row.group, rowKey, inner),
3450
3912
  placed: row.fields
@@ -3453,24 +3915,108 @@ function buildRowElement(row, fieldMap, defaultRowGap) {
3453
3915
  function collectUnplacedFields(fields, placed) {
3454
3916
  const result = [];
3455
3917
  for (const field of fields) {
3456
- if (React11.isValidElement(field) && field.key != null && !placed.has(String(field.key))) {
3918
+ if (React13.isValidElement(field) && field.key != null && !placed.has(String(field.key))) {
3457
3919
  result.push(field);
3458
3920
  }
3459
3921
  }
3460
3922
  return result;
3461
3923
  }
3462
- function layoutFieldsInRows(fields, rowDefs, defaultRowGap) {
3463
- const fieldMap = buildFieldMap(fields);
3464
- const placed = /* @__PURE__ */ new Set();
3924
+ function buildRows(rowDefs, fieldMap, defaultRowGap, placed) {
3465
3925
  const elements = [];
3466
3926
  for (const row of rowDefs) {
3467
3927
  const result = buildRowElement(row, fieldMap, defaultRowGap);
3468
- if (result == null) continue;
3469
- elements.push(result.element);
3470
- for (const name of result.placed) placed.add(name);
3928
+ for (const name of row.fields) placed.add(name);
3929
+ if (result != null) elements.push(result.element);
3471
3930
  }
3931
+ return elements;
3932
+ }
3933
+ function layoutFieldsInRows(fields, rowDefs, defaultRowGap) {
3934
+ const placed = /* @__PURE__ */ new Set();
3935
+ const elements = buildRows(
3936
+ rowDefs,
3937
+ buildFieldMap(fields),
3938
+ defaultRowGap,
3939
+ placed
3940
+ );
3472
3941
  return elements.concat(collectUnplacedFields(fields, placed));
3473
3942
  }
3943
+ function useRootFormData(ownData) {
3944
+ return useContext2(NestedFormContext)?.rootData ?? ownData;
3945
+ }
3946
+ function blurHandlerNeeded({
3947
+ deferStateUpdates,
3948
+ deferChanges,
3949
+ hasOnBlur,
3950
+ validationDisplay
3951
+ }) {
3952
+ return deferStateUpdates || deferChanges || hasOnBlur || validationDisplay === "onTouched";
3953
+ }
3954
+ function fieldErrorsVisible(validationDisplay, submitAttempted, isTouched) {
3955
+ if (validationDisplay === "always") return true;
3956
+ if (submitAttempted) return true;
3957
+ return validationDisplay === "onTouched" && !!isTouched;
3958
+ }
3959
+ function pointerRoot(pointer) {
3960
+ const token = pointer.split("/")[1] ?? "";
3961
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
3962
+ }
3963
+ function groupErrorCount(tab, fieldErrors) {
3964
+ if (fieldErrors.length === 0) return 0;
3965
+ const owned = new Set(tab.rows.flatMap((row) => row.fields));
3966
+ return fieldErrors.filter((e) => owned.has(pointerRoot(e.pointer))).length;
3967
+ }
3968
+ function groupVisibility(groupDefs, formData) {
3969
+ const visible = groupDefs.map(
3970
+ (tab) => evaluateUiRule(tab.rule, formData).visible
3971
+ );
3972
+ return { visible, allHidden: !visible.some(Boolean) };
3973
+ }
3974
+ function hiddenGroupFieldNames(groupDefs, formData) {
3975
+ const hidden = /* @__PURE__ */ new Set();
3976
+ const { visible, allHidden } = groupVisibility(groupDefs, formData);
3977
+ if (allHidden) return hidden;
3978
+ groupDefs.forEach((tab, i) => {
3979
+ if (visible[i]) return;
3980
+ for (const row of tab.rows) for (const f of row.fields) hidden.add(f);
3981
+ });
3982
+ return hidden;
3983
+ }
3984
+ function resolveGroupTemplate(templates, variant) {
3985
+ const registered = templates[groupTemplateKey(variant)];
3986
+ if (typeof registered === "function") {
3987
+ return registered;
3988
+ }
3989
+ return DEFAULT_TEMPLATES.TabsGroupTemplate;
3990
+ }
3991
+ function HiddenErrorNotice({
3992
+ count
3993
+ }) {
3994
+ if (count === 0) return null;
3995
+ return /* @__PURE__ */ jsxs23("p", { className: "flex items-center gap-1.5 text-sm text-destructive", children: [
3996
+ /* @__PURE__ */ jsx37(AlertCircle2, { className: "h-4 w-4 shrink-0" }),
3997
+ 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.`
3998
+ ] });
3999
+ }
4000
+ function layoutFieldsInGroups(fields, groupDefs, defaultRowGap, fieldErrors, formData, GroupTemplate) {
4001
+ const fieldMap = buildFieldMap(fields);
4002
+ const placed = /* @__PURE__ */ new Set();
4003
+ const { visible, allHidden } = groupVisibility(groupDefs, formData);
4004
+ const built = groupDefs.map((tab, index) => ({
4005
+ index,
4006
+ rows: buildRows(tab.rows, fieldMap, defaultRowGap, placed)
4007
+ }));
4008
+ const unplaced = collectUnplacedFields(fields, placed);
4009
+ if (allHidden) {
4010
+ return [...built.flatMap((b) => b.rows), ...unplaced];
4011
+ }
4012
+ const groups = built.filter((b) => visible[b.index]).map((b) => ({
4013
+ key: `group-${b.index}`,
4014
+ label: groupDefs[b.index].label,
4015
+ content: b.rows,
4016
+ errorCount: groupErrorCount(groupDefs[b.index], fieldErrors)
4017
+ }));
4018
+ return [/* @__PURE__ */ jsx37(GroupTemplate, { groups }, "form:groups"), ...unplaced];
4019
+ }
3474
4020
  function FormContainer({
3475
4021
  displayContents,
3476
4022
  showExtraProperties,
@@ -3482,19 +4028,22 @@ function FormContainer({
3482
4028
  disabled,
3483
4029
  hasChanges,
3484
4030
  isValid,
4031
+ saveWhenInvalid,
3485
4032
  saveLabel,
3486
4033
  cancelLabel,
3487
4034
  showCancel,
3488
4035
  leadingActions,
4036
+ hiddenErrorCount,
3489
4037
  children
3490
4038
  }) {
3491
4039
  if (displayContents) {
3492
- return /* @__PURE__ */ jsx28("div", { style: { display: "contents" }, children });
4040
+ return /* @__PURE__ */ jsx37("div", { style: { display: "contents" }, children });
3493
4041
  }
3494
- return /* @__PURE__ */ jsxs18(SimpleStack, { gap: "md", children: [
4042
+ return /* @__PURE__ */ jsxs23(SimpleStack, { gap: "md", children: [
3495
4043
  children,
3496
- showExtraProperties && /* @__PURE__ */ jsx28(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
3497
- showActions && /* @__PURE__ */ jsx28(
4044
+ showExtraProperties && /* @__PURE__ */ jsx37(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
4045
+ /* @__PURE__ */ jsx37(HiddenErrorNotice, { count: hiddenErrorCount }),
4046
+ showActions && /* @__PURE__ */ jsx37(
3498
4047
  FormActions,
3499
4048
  {
3500
4049
  onSave: handleSave,
@@ -3502,6 +4051,7 @@ function FormContainer({
3502
4051
  disabled,
3503
4052
  hasChanges,
3504
4053
  isValid,
4054
+ saveWhenInvalid,
3505
4055
  saveLabel,
3506
4056
  cancelLabel,
3507
4057
  showCancel,
@@ -3543,6 +4093,9 @@ function JsonSchemaFormRenderer(props) {
3543
4093
  orderedProperties,
3544
4094
  hasChanges,
3545
4095
  isValid,
4096
+ fieldErrors,
4097
+ touched,
4098
+ submitAttempted,
3546
4099
  handleFieldChange,
3547
4100
  handleFieldBlur,
3548
4101
  handleSave,
@@ -3560,22 +4113,23 @@ function JsonSchemaFormRenderer(props) {
3560
4113
  showActions,
3561
4114
  deferChanges,
3562
4115
  deferStateUpdates,
3563
- allowPristineSave
4116
+ allowPristineSave,
4117
+ refResolver: props.refResolver
3564
4118
  });
3565
4119
  const layoutLabelPlacement = uiSchema["x-ui-layout"]?.labelPlacement;
3566
- const globalOptions = useMemo7(
4120
+ const globalOptions = useMemo8(
3567
4121
  () => ({
3568
4122
  ...layoutLabelPlacement ? { labelPlacement: layoutLabelPlacement } : {},
3569
4123
  ...uiSchema["x-ui-globalOptions"] || {}
3570
4124
  }),
3571
4125
  [layoutLabelPlacement, uiSchema]
3572
4126
  );
3573
- const resolvedTemplates = useMemo7(
4127
+ const resolvedTemplates = useMemo8(
3574
4128
  () => ({ ...DEFAULT_TEMPLATES, ...templates }),
3575
4129
  [templates]
3576
4130
  );
3577
4131
  const nestedRenderer = useCallback8(
3578
- (props2) => /* @__PURE__ */ jsx28(
4132
+ (props2) => /* @__PURE__ */ jsx37(
3579
4133
  JsonSchemaFormRenderer,
3580
4134
  {
3581
4135
  ...props2,
@@ -3586,35 +4140,64 @@ function JsonSchemaFormRenderer(props) {
3586
4140
  ),
3587
4141
  [resolvedTemplates]
3588
4142
  );
3589
- const contextValue = useMemo7(
3590
- () => ({ renderer: nestedRenderer, templates: resolvedTemplates }),
3591
- [nestedRenderer, resolvedTemplates]
4143
+ const rootData = useRootFormData(displayData);
4144
+ const contextValue = useMemo8(
4145
+ () => ({
4146
+ renderer: nestedRenderer,
4147
+ templates: resolvedTemplates,
4148
+ rootData
4149
+ }),
4150
+ [nestedRenderer, resolvedTemplates, rootData]
3592
4151
  );
3593
- const needsBlurHandler = deferStateUpdates || !!onBlur;
3594
- const fields = useMemo7(
3595
- () => orderedProperties.map(([fieldName, property]) => /* @__PURE__ */ jsx28(
3596
- FormFieldRenderer,
3597
- {
3598
- fieldName,
3599
- property,
3600
- value: displayData[fieldName],
3601
- onChange: (v) => handleFieldChange(fieldName, v),
3602
- onBlur: needsBlurHandler ? () => handleFieldBlur(fieldName) : void 0,
3603
- required: isFieldRequired(resolvedSchema, fieldName),
3604
- disabled,
3605
- readOnly,
3606
- schema,
3607
- uiSchema: uiSchema[fieldName],
3608
- registry: widgets,
3609
- globalOptions,
3610
- formData: displayData,
3611
- idPrefix
3612
- },
3613
- fieldName
3614
- )),
4152
+ const validationDisplay = useValidationDisplay(props.validationDisplay);
4153
+ const saveWhenInvalid = useSaveWhenInvalid(props.saveWhenInvalid);
4154
+ const needsBlurHandler = blurHandlerNeeded({
4155
+ deferStateUpdates,
4156
+ deferChanges,
4157
+ hasOnBlur: !!onBlur,
4158
+ validationDisplay
4159
+ });
4160
+ const showFieldErrors = useCallback8(
4161
+ (fieldName) => fieldErrorsVisible(
4162
+ validationDisplay,
4163
+ submitAttempted,
4164
+ touched[fieldName]
4165
+ ),
4166
+ [validationDisplay, submitAttempted, touched]
4167
+ );
4168
+ const fields = useMemo8(
4169
+ () => orderedProperties.flatMap(([fieldName, property]) => {
4170
+ const rule = uiSchema[fieldName]?.["x-ui-rule"] ?? property["x-ui-rule"];
4171
+ const { visible, enabled } = evaluateUiRule(rule, rootData);
4172
+ if (!visible) return [];
4173
+ return [
4174
+ /* @__PURE__ */ jsx37(
4175
+ FormFieldRenderer,
4176
+ {
4177
+ fieldName,
4178
+ property,
4179
+ value: displayData[fieldName],
4180
+ onChange: (v) => handleFieldChange(fieldName, v),
4181
+ onBlur: needsBlurHandler ? () => handleFieldBlur(fieldName) : void 0,
4182
+ required: isFieldRequired(resolvedSchema, fieldName),
4183
+ disabled: disabled || !enabled,
4184
+ readOnly,
4185
+ schema,
4186
+ uiSchema: uiSchema[fieldName],
4187
+ registry: widgets,
4188
+ globalOptions,
4189
+ formData: displayData,
4190
+ idPrefix,
4191
+ rawErrors: showFieldErrors(fieldName) ? fieldErrors.filter((e) => pointerRoot(e.pointer) === fieldName).map((e) => e.message) : void 0
4192
+ },
4193
+ fieldName
4194
+ )
4195
+ ];
4196
+ }),
3615
4197
  [
3616
4198
  orderedProperties,
3617
4199
  displayData,
4200
+ rootData,
3618
4201
  resolvedSchema,
3619
4202
  disabled,
3620
4203
  readOnly,
@@ -3624,18 +4207,52 @@ function JsonSchemaFormRenderer(props) {
3624
4207
  globalOptions,
3625
4208
  idPrefix,
3626
4209
  needsBlurHandler,
4210
+ showFieldErrors,
4211
+ fieldErrors,
3627
4212
  handleFieldChange,
3628
4213
  handleFieldBlur
3629
4214
  ]
3630
4215
  );
3631
4216
  const layoutConfig = uiSchema["x-ui-layout"];
3632
4217
  const rowDefs = layoutConfig?.rows;
4218
+ const groupDefs = layoutConfig?.groups;
3633
4219
  const defaultRowGap = layoutConfig?.gap ?? 24;
3634
- const layoutFields = useMemo7(
3635
- () => rowDefs && rowDefs.length > 0 ? layoutFieldsInRows(fields, rowDefs, defaultRowGap) : fields,
3636
- [fields, rowDefs, defaultRowGap]
4220
+ const groupTemplate = resolveGroupTemplate(
4221
+ resolvedTemplates,
4222
+ layoutConfig?.variant
3637
4223
  );
3638
- return /* @__PURE__ */ jsx28(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx28(
4224
+ const layoutFields = useMemo8(() => {
4225
+ if (groupDefs && groupDefs.length > 0) {
4226
+ return layoutFieldsInGroups(
4227
+ fields,
4228
+ groupDefs,
4229
+ defaultRowGap,
4230
+ fieldErrors,
4231
+ // Tab rules are x-ui-rule too: root-anchored like field rules.
4232
+ rootData,
4233
+ groupTemplate
4234
+ );
4235
+ }
4236
+ if (rowDefs && rowDefs.length > 0) {
4237
+ return layoutFieldsInRows(fields, rowDefs, defaultRowGap);
4238
+ }
4239
+ return fields;
4240
+ }, [
4241
+ fields,
4242
+ rowDefs,
4243
+ groupDefs,
4244
+ defaultRowGap,
4245
+ fieldErrors,
4246
+ rootData,
4247
+ groupTemplate
4248
+ ]);
4249
+ const hiddenErrorCount = useMemo8(() => {
4250
+ if (!groupDefs?.length || fieldErrors.length === 0) return 0;
4251
+ const hidden = hiddenGroupFieldNames(groupDefs, rootData);
4252
+ if (hidden.size === 0) return 0;
4253
+ return fieldErrors.filter((e) => hidden.has(pointerRoot(e.pointer))).length;
4254
+ }, [groupDefs, fieldErrors, rootData]);
4255
+ return /* @__PURE__ */ jsx37(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx37(
3639
4256
  FormContainer,
3640
4257
  {
3641
4258
  displayContents,
@@ -3648,17 +4265,19 @@ function JsonSchemaFormRenderer(props) {
3648
4265
  disabled,
3649
4266
  hasChanges: hasChanges || allowPristineSave,
3650
4267
  isValid,
4268
+ saveWhenInvalid,
3651
4269
  saveLabel,
3652
4270
  cancelLabel,
3653
4271
  showCancel,
3654
4272
  leadingActions,
4273
+ hiddenErrorCount,
3655
4274
  children: layoutFields
3656
4275
  }
3657
4276
  ) });
3658
4277
  }
3659
4278
 
3660
4279
  // src/hooks/useResolvedSchema.ts
3661
- import { useMemo as useMemo8 } from "react";
4280
+ import { useMemo as useMemo9 } from "react";
3662
4281
  import {
3663
4282
  getSchemaProperties as getSchemaProperties2,
3664
4283
  resolveSchema as resolveSchema2
@@ -3668,11 +4287,11 @@ function useResolvedSchema({
3668
4287
  formData,
3669
4288
  uiSchema
3670
4289
  }) {
3671
- const resolvedSchema = useMemo8(
4290
+ const resolvedSchema = useMemo9(
3672
4291
  () => resolveSchema2(schema, formData),
3673
4292
  [schema, formData]
3674
4293
  );
3675
- const orderedProperties = useMemo8(() => {
4294
+ const orderedProperties = useMemo9(() => {
3676
4295
  const properties = getSchemaProperties2(resolvedSchema);
3677
4296
  const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
3678
4297
  const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
@@ -3686,6 +4305,9 @@ export {
3686
4305
  DEFAULT_TEMPLATES,
3687
4306
  DEFAULT_WIDGETS,
3688
4307
  DEFAULT_WIDGET_MAP,
4308
+ DateTimeWidget,
4309
+ DateWidget,
4310
+ DisplayWidget,
3689
4311
  EntitySelectWidget,
3690
4312
  ExtraPropertiesCard_default as ExtraPropertiesCard,
3691
4313
  FormActions,
@@ -3699,18 +4321,25 @@ export {
3699
4321
  FormSelect,
3700
4322
  FormTagsInput,
3701
4323
  FormTextarea,
4324
+ GroupErrorBadge,
3702
4325
  JsonEditorWidget,
3703
4326
  JsonSchemaFormRenderer,
3704
4327
  NumberWidget,
3705
4328
  ObjectWidget,
4329
+ OneOfWidget,
3706
4330
  QuantityWidget,
4331
+ SectionsGroupTemplate,
3707
4332
  SelectWidget,
4333
+ StepperGroupTemplate,
4334
+ TabsGroupTemplate,
3708
4335
  TagsInputWidget,
3709
4336
  TextInputWidget,
3710
4337
  TextareaWidget,
4338
+ TimeWidget,
3711
4339
  firstError,
3712
4340
  getAriaDescribedBy,
3713
4341
  getDefaultWidgetForType,
4342
+ groupTemplateKey,
3714
4343
  resolveWidget,
3715
4344
  useFormEngine,
3716
4345
  useNestedFormContext,