@petrarca/sonnet-forms 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,
@@ -1467,6 +1535,9 @@ var ExtraPropertiesCard_default = ExtraPropertiesCard;
1467
1535
  // src/JsonSchemaFormRenderer.tsx
1468
1536
  import React11, { useCallback as useCallback8, useMemo as useMemo7 } from "react";
1469
1537
  import { SimpleStack } from "@petrarca/sonnet-ui";
1538
+ import {
1539
+ WIDTH_TOKEN_REMS
1540
+ } from "@petrarca/sonnet-core/schema";
1470
1541
  import { isFieldRequired } from "@petrarca/sonnet-core/schema";
1471
1542
  import { FORM_ITEM_ID_FIELD as FORM_ITEM_ID_FIELD2 } from "@petrarca/sonnet-core";
1472
1543
 
@@ -1510,6 +1581,7 @@ function TextInputWidget({
1510
1581
  onChange: (event) => onChange(event.currentTarget.value),
1511
1582
  onBlur: () => onBlur?.(id, value),
1512
1583
  label,
1584
+ labelPlacement: options?.labelPlacement,
1513
1585
  description,
1514
1586
  placeholder,
1515
1587
  required,
@@ -1557,6 +1629,7 @@ function TextareaWidget({
1557
1629
  onChange: (event) => onChange(event.currentTarget.value),
1558
1630
  onBlur: () => onBlur?.(id, value),
1559
1631
  label,
1632
+ labelPlacement: options?.labelPlacement,
1560
1633
  description,
1561
1634
  placeholder,
1562
1635
  required,
@@ -1604,6 +1677,7 @@ function NumberWidget({
1604
1677
  onChange: (val) => onChange(val),
1605
1678
  onBlur: () => onBlur?.(id, value),
1606
1679
  label,
1680
+ labelPlacement: options.labelPlacement,
1607
1681
  description,
1608
1682
  placeholder,
1609
1683
  required,
@@ -1735,6 +1809,7 @@ function SelectWidget(props) {
1735
1809
  value: normalizeMultiValue(value),
1736
1810
  onChange: (newValue) => onChange(newValue || []),
1737
1811
  label,
1812
+ labelPlacement: widgetOptions.labelPlacement,
1738
1813
  description,
1739
1814
  placeholder,
1740
1815
  required,
@@ -1758,6 +1833,7 @@ function SelectWidget(props) {
1758
1833
  value: normalizeSingleValue(value),
1759
1834
  onChange: (val) => onChange(val || ""),
1760
1835
  label,
1836
+ labelPlacement: widgetOptions.labelPlacement,
1761
1837
  description,
1762
1838
  placeholder,
1763
1839
  required,
@@ -1799,6 +1875,7 @@ function TagsInputWidget({
1799
1875
  value: tags,
1800
1876
  onChange: (newTags) => onChange(newTags),
1801
1877
  label,
1878
+ labelPlacement: options?.labelPlacement,
1802
1879
  description,
1803
1880
  placeholder: placeholder ?? "Type and press Enter",
1804
1881
  required,
@@ -1880,11 +1957,14 @@ function resolveMinChars(options) {
1880
1957
  }
1881
1958
  function buildRestConfig(options) {
1882
1959
  if (!("endpoint" in options) || !options.endpoint) return null;
1960
+ const rawExtra = options.extraParams;
1961
+ const extraParams = rawExtra && typeof rawExtra === "object" ? rawExtra : void 0;
1883
1962
  return {
1884
1963
  endpoint: String(options.endpoint),
1885
1964
  valueKey: String(options.valueKey ?? "id"),
1886
1965
  labelKey: String(options.labelKey ?? "name"),
1887
- searchKey: options.searchKey ? String(options.searchKey) : void 0
1966
+ searchKey: options.searchKey ? String(options.searchKey) : void 0,
1967
+ extraParams
1888
1968
  };
1889
1969
  }
1890
1970
  function buildGraphqlConfig(options) {
@@ -1948,12 +2028,13 @@ function EntitySelectWidget({
1948
2028
  mode: selectMode,
1949
2029
  minChars: selectMinChars,
1950
2030
  value: typeof value === "string" ? value : null,
1951
- onChange: (v) => onChange(v ?? ""),
2031
+ onChange: (v) => onChange(v ?? null),
1952
2032
  onBlur: () => onBlur?.(id, value),
1953
2033
  label: label || false,
1954
2034
  description,
1955
2035
  placeholder,
1956
2036
  required,
2037
+ clearable: !required,
1957
2038
  disabled: disabled || readonly,
1958
2039
  error: firstError(rawErrors)
1959
2040
  }
@@ -2166,7 +2247,7 @@ function ObjectWidget({
2166
2247
 
2167
2248
  // src/widgets/ArrayWidget.tsx
2168
2249
  import { useCallback as useCallback5, useMemo as useMemo5 } from "react";
2169
- import { Plus } from "lucide-react";
2250
+ import { Plus as Plus2 } from "lucide-react";
2170
2251
  import { Button as Button5 } from "@petrarca/sonnet-ui";
2171
2252
  import { applySchemaDefaults } from "@petrarca/sonnet-core/schema";
2172
2253
  import { FORM_ITEM_ID_FIELD, generateId } from "@petrarca/sonnet-core";
@@ -2182,53 +2263,61 @@ function DefaultArrayItemTemplate({
2182
2263
  canRemove,
2183
2264
  orderable,
2184
2265
  disabled,
2266
+ fid,
2185
2267
  onRemove,
2186
2268
  onMoveUp,
2187
2269
  onMoveDown
2188
2270
  }) {
2189
2271
  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
- ] });
2272
+ return /* @__PURE__ */ jsxs14(
2273
+ "div",
2274
+ {
2275
+ "data-fid": fid,
2276
+ className: "relative flex gap-2 rounded-md border border-border/50 bg-muted/20 p-3",
2277
+ children: [
2278
+ /* @__PURE__ */ jsx22("div", { className: "flex-1 min-w-0", children }),
2279
+ showControls && /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1 pt-0.5", children: [
2280
+ orderable && index > 0 && /* @__PURE__ */ jsx22(
2281
+ Button4,
2282
+ {
2283
+ type: "button",
2284
+ variant: "ghost",
2285
+ size: "compact",
2286
+ disabled,
2287
+ onClick: onMoveUp,
2288
+ title: "Move up",
2289
+ children: /* @__PURE__ */ jsx22(ChevronUp, { className: "h-3.5 w-3.5" })
2290
+ }
2291
+ ),
2292
+ orderable && index < total - 1 && /* @__PURE__ */ jsx22(
2293
+ Button4,
2294
+ {
2295
+ type: "button",
2296
+ variant: "ghost",
2297
+ size: "compact",
2298
+ disabled,
2299
+ onClick: onMoveDown,
2300
+ title: "Move down",
2301
+ children: /* @__PURE__ */ jsx22(ChevronDown3, { className: "h-3.5 w-3.5" })
2302
+ }
2303
+ ),
2304
+ canRemove && /* @__PURE__ */ jsx22(
2305
+ Button4,
2306
+ {
2307
+ type: "button",
2308
+ variant: "ghost",
2309
+ size: "compact",
2310
+ disabled,
2311
+ onClick: onRemove,
2312
+ title: "Remove item",
2313
+ className: "text-muted-foreground hover:text-destructive",
2314
+ children: /* @__PURE__ */ jsx22(Trash2, { className: "h-3.5 w-3.5" })
2315
+ }
2316
+ )
2317
+ ] })
2318
+ ]
2319
+ }
2320
+ );
2232
2321
  }
2233
2322
 
2234
2323
  // src/templates/gridUtils.ts
@@ -2494,6 +2583,7 @@ function ArrayWidgetInner({
2494
2583
  disabled,
2495
2584
  layout,
2496
2585
  fieldCount,
2586
+ fid: item[FORM_ITEM_ID_FIELD],
2497
2587
  onRemove: () => onRemove(index),
2498
2588
  onMoveUp: index > 0 ? () => onMove(index, index - 1) : void 0,
2499
2589
  onMoveDown: index < items.length - 1 ? () => onMove(index, index + 1) : void 0,
@@ -2528,7 +2618,7 @@ function ArrayWidgetInner({
2528
2618
  disabled,
2529
2619
  className: "gap-1.5",
2530
2620
  children: [
2531
- /* @__PURE__ */ jsx24(Plus, { className: "h-3.5 w-3.5" }),
2621
+ /* @__PURE__ */ jsx24(Plus2, { className: "h-3.5 w-3.5" }),
2532
2622
  display.addLabel
2533
2623
  ]
2534
2624
  }
@@ -2542,6 +2632,13 @@ import { jsx as jsx25 } from "react/jsx-runtime";
2542
2632
  function isUnitOption(u) {
2543
2633
  return typeof u === "object" && u !== null && typeof u.value === "string" && typeof u.label === "string";
2544
2634
  }
2635
+ function parseQuantityOptions(options) {
2636
+ const units = Array.isArray(options.units) ? options.units.filter(isUnitOption) : [];
2637
+ const precision = typeof options.precision === "number" ? options.precision : void 0;
2638
+ const step = typeof options.step === "number" ? options.step : void 0;
2639
+ const unitPosition = options.unitPosition === "prefix" || options.unitPosition === "suffix" ? options.unitPosition : void 0;
2640
+ return { units, precision, step, unitPosition };
2641
+ }
2545
2642
  function QuantityWidget({
2546
2643
  id,
2547
2644
  name,
@@ -2560,10 +2657,12 @@ function QuantityWidget({
2560
2657
  options = {}
2561
2658
  }) {
2562
2659
  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;
2660
+ const {
2661
+ units,
2662
+ precision,
2663
+ step,
2664
+ unitPosition: unitPositionOpt
2665
+ } = parseQuantityOptions(options);
2567
2666
  const quantityValue = value != null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2568
2667
  const valueRef = useRef4(value);
2569
2668
  valueRef.current = value;
@@ -2585,6 +2684,7 @@ function QuantityWidget({
2585
2684
  min: propertySchema.minimum,
2586
2685
  max: propertySchema.maximum,
2587
2686
  label,
2687
+ labelPlacement: options?.labelPlacement,
2588
2688
  description,
2589
2689
  error,
2590
2690
  placeholder,
@@ -2741,6 +2841,7 @@ function HorizontalArrayItemTemplate({
2741
2841
  disabled,
2742
2842
  layout,
2743
2843
  fieldCount,
2844
+ fid,
2744
2845
  onRemove,
2745
2846
  onMoveUp,
2746
2847
  onMoveDown
@@ -2757,6 +2858,7 @@ function HorizontalArrayItemTemplate({
2757
2858
  return /* @__PURE__ */ jsxs17(
2758
2859
  "div",
2759
2860
  {
2861
+ "data-fid": fid,
2760
2862
  style: {
2761
2863
  display: "grid",
2762
2864
  gridTemplateColumns: template,
@@ -2820,6 +2922,7 @@ var DEFAULT_TEMPLATES = {
2820
2922
  };
2821
2923
 
2822
2924
  // src/FormFieldRenderer.tsx
2925
+ import { warnLog } from "@petrarca/sonnet-core";
2823
2926
  import {
2824
2927
  getFieldTitle,
2825
2928
  getFieldHelpText,
@@ -2837,6 +2940,29 @@ function resolveEffectiveLabel(label, uiTitle, globalLabelOption) {
2837
2940
  if (uiTitle === false) return "";
2838
2941
  return label;
2839
2942
  }
2943
+ var FIELD_WIDTH_CLASS = {
2944
+ sm: "w-32",
2945
+ md: "w-48",
2946
+ lg: "w-64",
2947
+ full: "w-full"
2948
+ };
2949
+ function applyFieldWidth(widget, uiSchema, mergedOptions) {
2950
+ const candidate = uiSchema?.["x-ui-width"] ?? mergedOptions.width;
2951
+ if (candidate && !(candidate in FIELD_WIDTH_CLASS)) {
2952
+ warnLog(
2953
+ `[FormFieldRenderer] Unknown x-ui-width value: "${candidate}". Falling back to "full".`
2954
+ );
2955
+ }
2956
+ const width = candidate && candidate in FIELD_WIDTH_CLASS ? candidate : "full";
2957
+ if (width === "full") return widget;
2958
+ return /* @__PURE__ */ jsx27("div", { className: FIELD_WIDTH_CLASS[width], children: widget });
2959
+ }
2960
+ function resolvePresentationText(property, uiSchema) {
2961
+ return {
2962
+ description: uiSchema?.["x-ui-description"] ?? getFieldHelpText(property),
2963
+ placeholder: uiSchema?.["x-ui-placeholder"] ?? getFieldPlaceholder(property)
2964
+ };
2965
+ }
2840
2966
  function resolveFieldFlags(fieldOptions, globalOptions, disabled, readOnly) {
2841
2967
  return {
2842
2968
  disabled: fieldOptions.disabled ?? globalOptions.disabled ?? disabled,
@@ -2856,11 +2982,14 @@ function FormFieldRenderer({
2856
2982
  uiSchema,
2857
2983
  registry,
2858
2984
  globalOptions = {},
2859
- formData
2985
+ formData,
2986
+ idPrefix = "field"
2860
2987
  }) {
2861
2988
  const label = getFieldTitle(fieldName, property);
2862
- const description = uiSchema?.["x-ui-description"] ?? getFieldHelpText(property);
2863
- const placeholder = uiSchema?.["x-ui-placeholder"] ?? getFieldPlaceholder(property);
2989
+ const { description, placeholder } = resolvePresentationText(
2990
+ property,
2991
+ uiSchema
2992
+ );
2864
2993
  const Widget = resolveWidget(property, uiSchema, registry);
2865
2994
  const mergedOptions = mergeFieldOptions(globalOptions, property, uiSchema);
2866
2995
  if (mergedOptions.hidden) return null;
@@ -2876,10 +3005,10 @@ function FormFieldRenderer({
2876
3005
  uiSchema?.["x-ui-title"],
2877
3006
  globalOptions.label
2878
3007
  );
2879
- return /* @__PURE__ */ jsx27(
3008
+ const widget = /* @__PURE__ */ jsx27(
2880
3009
  Widget,
2881
3010
  {
2882
- id: `field-${fieldName}`,
3011
+ id: `${idPrefix}-${fieldName}`,
2883
3012
  name: fieldName,
2884
3013
  value,
2885
3014
  onChange,
@@ -2898,6 +3027,7 @@ function FormFieldRenderer({
2898
3027
  formData
2899
3028
  }
2900
3029
  );
3030
+ return applyFieldWidth(widget, uiSchema, mergedOptions);
2901
3031
  }
2902
3032
 
2903
3033
  // src/hooks/useFormEngine.ts
@@ -2968,7 +3098,8 @@ function useFormEngine({
2968
3098
  readOnly = false,
2969
3099
  showActions = false,
2970
3100
  deferChanges = false,
2971
- deferStateUpdates = false
3101
+ deferStateUpdates = false,
3102
+ allowPristineSave = false
2972
3103
  }) {
2973
3104
  const refResolvedSchema = useMemo6(() => resolveRefs(schema), [schema]);
2974
3105
  const [originalData, setOriginalData] = useState7(
@@ -3154,7 +3285,7 @@ function useFormEngine({
3154
3285
  ]
3155
3286
  );
3156
3287
  const handleSave = useCallback7(() => {
3157
- if (!hasChanges) return;
3288
+ if (!hasChanges && !allowPristineSave) return;
3158
3289
  const finalData = deferStateUpdates && Object.keys(pendingChanges).length > 0 ? { ...formData, ...pendingChanges } : formData;
3159
3290
  const completeData = { ...nonSchemaData, ...finalData };
3160
3291
  const validation = validateFormData(resolvedSchema, finalData);
@@ -3171,6 +3302,7 @@ function useFormEngine({
3171
3302
  }
3172
3303
  }, [
3173
3304
  hasChanges,
3305
+ allowPristineSave,
3174
3306
  resolvedSchema,
3175
3307
  formData,
3176
3308
  originalData,
@@ -3232,7 +3364,8 @@ function resolveFlagDefaults(props) {
3232
3364
  deferStateUpdates: props.deferStateUpdates ?? false,
3233
3365
  showCancel: props.showCancel ?? true,
3234
3366
  showExtraProperties: props.showExtraProperties ?? true,
3235
- displayContents: props.displayContents ?? false
3367
+ displayContents: props.displayContents ?? false,
3368
+ allowPristineSave: props.allowPristineSave ?? false
3236
3369
  };
3237
3370
  }
3238
3371
  function resolveValueDefaults(props) {
@@ -3271,24 +3404,49 @@ function buildFieldMap(fields) {
3271
3404
  }
3272
3405
  return map;
3273
3406
  }
3407
+ var COLUMN_TRACK = WIDTH_TOKEN_REMS;
3408
+ function resolveGridColumns(row, fieldCount) {
3409
+ if (row.columns) return row.columns;
3410
+ if (row.widths && row.widths.length > 0) {
3411
+ return Array.from(
3412
+ { length: fieldCount },
3413
+ (_, i) => COLUMN_TRACK[row.widths?.[i] ?? "full"]
3414
+ ).join(" ");
3415
+ }
3416
+ return `repeat(${fieldCount}, 1fr)`;
3417
+ }
3418
+ function wrapInGroup(group, key, content) {
3419
+ if (!group) return content;
3420
+ return /* @__PURE__ */ jsxs18(
3421
+ "fieldset",
3422
+ {
3423
+ className: "rounded-md border border-border/60 px-3 pb-3 pt-1",
3424
+ children: [
3425
+ /* @__PURE__ */ jsx28("legend", { className: "px-1 text-sm font-medium text-muted-foreground", children: group }),
3426
+ content
3427
+ ]
3428
+ },
3429
+ `group:${key}`
3430
+ );
3431
+ }
3274
3432
  function buildRowElement(row, fieldMap, defaultRowGap) {
3275
3433
  const rowFields = row.fields.map((name) => fieldMap.get(name)).filter((el) => el != null);
3276
3434
  if (rowFields.length === 0) return null;
3277
- const gridColumns = row.columns ?? `repeat(${rowFields.length}, 1fr)`;
3278
3435
  const rowGap = row.gap ?? defaultRowGap;
3436
+ const rowKey = row.fields.join(",");
3437
+ const style = row.arrange === "flow" ? {
3438
+ display: "flex",
3439
+ flexWrap: "wrap",
3440
+ alignItems: "center",
3441
+ gap: `${rowGap}px`
3442
+ } : {
3443
+ display: "grid",
3444
+ gridTemplateColumns: resolveGridColumns(row, rowFields.length),
3445
+ gap: `${rowGap}px`
3446
+ };
3447
+ const inner = /* @__PURE__ */ jsx28("div", { style, children: rowFields }, `row:${rowKey}`);
3279
3448
  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
- ),
3449
+ element: wrapInGroup(row.group, rowKey, inner),
3292
3450
  placed: row.fields
3293
3451
  };
3294
3452
  }
@@ -3333,7 +3491,7 @@ function FormContainer({
3333
3491
  if (displayContents) {
3334
3492
  return /* @__PURE__ */ jsx28("div", { style: { display: "contents" }, children });
3335
3493
  }
3336
- return /* @__PURE__ */ jsxs18(SimpleStack, { gap: 24, children: [
3494
+ return /* @__PURE__ */ jsxs18(SimpleStack, { gap: "md", children: [
3337
3495
  children,
3338
3496
  showExtraProperties && /* @__PURE__ */ jsx28(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
3339
3497
  showActions && /* @__PURE__ */ jsx28(
@@ -3362,7 +3520,8 @@ function JsonSchemaFormRenderer(props) {
3362
3520
  onUpdate,
3363
3521
  onCancel,
3364
3522
  templates,
3365
- leadingActions
3523
+ leadingActions,
3524
+ idPrefix = "field"
3366
3525
  } = props;
3367
3526
  const {
3368
3527
  disabled,
@@ -3372,7 +3531,8 @@ function JsonSchemaFormRenderer(props) {
3372
3531
  deferStateUpdates,
3373
3532
  showCancel,
3374
3533
  showExtraProperties,
3375
- displayContents
3534
+ displayContents,
3535
+ allowPristineSave
3376
3536
  } = resolveFlagDefaults(props);
3377
3537
  const { saveLabel, cancelLabel, widgets, uiSchema } = resolveValueDefaults(props);
3378
3538
  const { wrappedOnChange, wrappedOnBlur, wrappedOnUpdate } = useWrappedCallbacks(onChange, onBlur, onUpdate);
@@ -3399,9 +3559,17 @@ function JsonSchemaFormRenderer(props) {
3399
3559
  readOnly,
3400
3560
  showActions,
3401
3561
  deferChanges,
3402
- deferStateUpdates
3562
+ deferStateUpdates,
3563
+ allowPristineSave
3403
3564
  });
3404
- const globalOptions = uiSchema["x-ui-globalOptions"] || {};
3565
+ const layoutLabelPlacement = uiSchema["x-ui-layout"]?.labelPlacement;
3566
+ const globalOptions = useMemo7(
3567
+ () => ({
3568
+ ...layoutLabelPlacement ? { labelPlacement: layoutLabelPlacement } : {},
3569
+ ...uiSchema["x-ui-globalOptions"] || {}
3570
+ }),
3571
+ [layoutLabelPlacement, uiSchema]
3572
+ );
3405
3573
  const resolvedTemplates = useMemo7(
3406
3574
  () => ({ ...DEFAULT_TEMPLATES, ...templates }),
3407
3575
  [templates]
@@ -3423,25 +3591,43 @@ function JsonSchemaFormRenderer(props) {
3423
3591
  [nestedRenderer, resolvedTemplates]
3424
3592
  );
3425
3593
  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),
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
+ )),
3615
+ [
3616
+ orderedProperties,
3617
+ displayData,
3618
+ resolvedSchema,
3435
3619
  disabled,
3436
3620
  readOnly,
3437
3621
  schema,
3438
- uiSchema: uiSchema[fieldName],
3439
- registry: widgets,
3622
+ uiSchema,
3623
+ widgets,
3440
3624
  globalOptions,
3441
- formData: displayData
3442
- },
3443
- fieldName
3444
- ));
3625
+ idPrefix,
3626
+ needsBlurHandler,
3627
+ handleFieldChange,
3628
+ handleFieldBlur
3629
+ ]
3630
+ );
3445
3631
  const layoutConfig = uiSchema["x-ui-layout"];
3446
3632
  const rowDefs = layoutConfig?.rows;
3447
3633
  const defaultRowGap = layoutConfig?.gap ?? 24;
@@ -3460,7 +3646,7 @@ function JsonSchemaFormRenderer(props) {
3460
3646
  handleSave,
3461
3647
  handleCancel,
3462
3648
  disabled,
3463
- hasChanges,
3649
+ hasChanges: hasChanges || allowPristineSave,
3464
3650
  isValid,
3465
3651
  saveLabel,
3466
3652
  cancelLabel,