@petrarca/sonnet-forms 0.7.0 → 0.9.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
@@ -446,6 +446,13 @@ import {
446
446
  import { Popover, PopoverContent, PopoverTrigger } from "@petrarca/sonnet-ui";
447
447
 
448
448
  // src/formSelectUtils.ts
449
+ import { extractEnumValues } from "@petrarca/sonnet-core/schema";
450
+ function buildSelectOptions(propertySchema, enumNames) {
451
+ return extractEnumValues(propertySchema).map((val) => {
452
+ const key = String(val);
453
+ return { value: key, label: enumNames[key] || key };
454
+ });
455
+ }
449
456
  function canCreateFromSearch(allowCreate, searchQuery, options) {
450
457
  const trimmed = searchQuery.trim();
451
458
  if (!allowCreate || trimmed.length === 0) return false;
@@ -503,7 +510,7 @@ function SelectDropdown({
503
510
  searchQuery,
504
511
  onSearchChange,
505
512
  loading,
506
- emptyMessage,
513
+ emptyMessage: emptyMessage2,
507
514
  options,
508
515
  value,
509
516
  onSelect,
@@ -521,7 +528,7 @@ function SelectDropdown({
521
528
  }
522
529
  ),
523
530
  /* @__PURE__ */ jsxs5(CommandList, { children: [
524
- !showCreate && /* @__PURE__ */ jsx6(CommandEmpty, { children: loading ? "Loading..." : emptyMessage }),
531
+ !showCreate && /* @__PURE__ */ jsx6(CommandEmpty, { children: loading ? "Loading..." : emptyMessage2 }),
525
532
  /* @__PURE__ */ jsxs5(CommandGroup, { children: [
526
533
  options.filter(
527
534
  (option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())
@@ -596,7 +603,7 @@ function FormSelect(props) {
596
603
  const {
597
604
  placeholder,
598
605
  searchPlaceholder,
599
- emptyMessage,
606
+ emptyMessage: emptyMessage2,
600
607
  clearable,
601
608
  disabled,
602
609
  readOnly,
@@ -675,7 +682,7 @@ function FormSelect(props) {
675
682
  searchQuery,
676
683
  onSearchChange: setSearchQuery,
677
684
  loading,
678
- emptyMessage,
685
+ emptyMessage: emptyMessage2,
679
686
  options,
680
687
  value,
681
688
  onSelect: handleSelect,
@@ -801,7 +808,7 @@ function MultiSelectDropdown({
801
808
  searchQuery,
802
809
  onSearchChange,
803
810
  loading,
804
- emptyMessage,
811
+ emptyMessage: emptyMessage2,
805
812
  options,
806
813
  value,
807
814
  onSelect
@@ -816,7 +823,7 @@ function MultiSelectDropdown({
816
823
  }
817
824
  ),
818
825
  /* @__PURE__ */ jsxs6(CommandList2, { children: [
819
- /* @__PURE__ */ jsx7(CommandEmpty2, { children: loading ? "Loading..." : emptyMessage }),
826
+ /* @__PURE__ */ jsx7(CommandEmpty2, { children: loading ? "Loading..." : emptyMessage2 }),
820
827
  /* @__PURE__ */ jsx7(CommandGroup2, { children: options.filter(
821
828
  (option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())
822
829
  ).map((option) => /* @__PURE__ */ jsxs6(
@@ -930,7 +937,7 @@ function FormMultiSelect(props) {
930
937
  const {
931
938
  placeholder,
932
939
  searchPlaceholder,
933
- emptyMessage,
940
+ emptyMessage: emptyMessage2,
934
941
  clearable,
935
942
  disabled,
936
943
  readOnly,
@@ -1016,7 +1023,7 @@ function FormMultiSelect(props) {
1016
1023
  searchQuery,
1017
1024
  onSearchChange: setSearchQuery,
1018
1025
  loading,
1019
- emptyMessage,
1026
+ emptyMessage: emptyMessage2,
1020
1027
  options,
1021
1028
  value,
1022
1029
  onSelect: handleSelect
@@ -1033,18 +1040,38 @@ function FormMultiSelect(props) {
1033
1040
 
1034
1041
  // src/FormTagsInput.tsx
1035
1042
  import * as React7 from "react";
1036
- import { X as X3 } from "lucide-react";
1037
1043
  import { cn as cn8 } from "@petrarca/sonnet-core";
1038
1044
  import { Input as Input3 } from "@petrarca/sonnet-ui";
1045
+
1046
+ // src/TagBadge.tsx
1047
+ import { X as X3 } from "lucide-react";
1039
1048
  import { Badge as Badge2 } from "@petrarca/sonnet-ui";
1040
1049
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1041
- function TagBadge({ tag, index, isDisabled, onRemove }) {
1050
+ function TagBadge({
1051
+ tag,
1052
+ index,
1053
+ isDisabled,
1054
+ onRemove,
1055
+ onSelect
1056
+ }) {
1042
1057
  return /* @__PURE__ */ jsxs7(Badge2, { variant: "secondary", className: "gap-1", children: [
1043
- tag,
1058
+ onSelect && !isDisabled ? /* @__PURE__ */ jsx8(
1059
+ "button",
1060
+ {
1061
+ type: "button",
1062
+ className: "outline-none hover:underline focus:underline",
1063
+ onClick: (e) => {
1064
+ e.stopPropagation();
1065
+ onSelect(index);
1066
+ },
1067
+ children: tag
1068
+ }
1069
+ ) : tag,
1044
1070
  !isDisabled && /* @__PURE__ */ jsx8(
1045
1071
  "button",
1046
1072
  {
1047
1073
  type: "button",
1074
+ "aria-label": `Remove ${tag}`,
1048
1075
  className: "ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
1049
1076
  onKeyDown: (e) => {
1050
1077
  if (e.key === "Enter") {
@@ -1065,6 +1092,9 @@ function TagBadge({ tag, index, isDisabled, onRemove }) {
1065
1092
  )
1066
1093
  ] });
1067
1094
  }
1095
+
1096
+ // src/FormTagsInput.tsx
1097
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1068
1098
  function useTagManagement(value, onChange, allowDuplicates, maxTags, splitKeys) {
1069
1099
  const [inputValue, setInputValue] = React7.useState("");
1070
1100
  const addTag = React7.useCallback(
@@ -1146,7 +1176,7 @@ var FormTagsInput = React7.forwardRef((props, ref) => {
1146
1176
  const handleContainerClick = () => {
1147
1177
  inputRef.current?.focus();
1148
1178
  };
1149
- return /* @__PURE__ */ jsx8(
1179
+ return /* @__PURE__ */ jsx9(
1150
1180
  FormFieldWrapper,
1151
1181
  {
1152
1182
  inputId: fieldId,
@@ -1156,7 +1186,7 @@ var FormTagsInput = React7.forwardRef((props, ref) => {
1156
1186
  required,
1157
1187
  labelPlacement,
1158
1188
  className: wrapperClassName,
1159
- children: /* @__PURE__ */ jsxs7(
1189
+ children: /* @__PURE__ */ jsxs8(
1160
1190
  "div",
1161
1191
  {
1162
1192
  className: cn8(
@@ -1166,7 +1196,7 @@ var FormTagsInput = React7.forwardRef((props, ref) => {
1166
1196
  ),
1167
1197
  onClick: handleContainerClick,
1168
1198
  children: [
1169
- value.map((tag, index) => /* @__PURE__ */ jsx8(
1199
+ value.map((tag, index) => /* @__PURE__ */ jsx9(
1170
1200
  TagBadge,
1171
1201
  {
1172
1202
  tag,
@@ -1176,7 +1206,7 @@ var FormTagsInput = React7.forwardRef((props, ref) => {
1176
1206
  },
1177
1207
  index
1178
1208
  )),
1179
- /* @__PURE__ */ jsx8(
1209
+ /* @__PURE__ */ jsx9(
1180
1210
  Input3,
1181
1211
  {
1182
1212
  ref: inputRef,
@@ -1198,16 +1228,415 @@ var FormTagsInput = React7.forwardRef((props, ref) => {
1198
1228
  });
1199
1229
  FormTagsInput.displayName = "FormTagsInput";
1200
1230
 
1231
+ // src/FormKeyValueInput.tsx
1232
+ import * as React8 from "react";
1233
+ import { Plus as Plus2, X as X4 } from "lucide-react";
1234
+ import { cn as cn9 } from "@petrarca/sonnet-core";
1235
+ import { Badge as Badge3, Button as Button3, IconButton, Input as Input4 } from "@petrarca/sonnet-ui";
1236
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1237
+ function usePairEditor(value, onChange) {
1238
+ const [key, setKey] = React8.useState("");
1239
+ const [val, setVal] = React8.useState("");
1240
+ const [problem, setProblem] = React8.useState(null);
1241
+ const add = () => {
1242
+ const k = key.trim();
1243
+ const v = val.trim();
1244
+ if (!k || !v) {
1245
+ setProblem(!k ? "Enter a key." : "Enter a value.");
1246
+ return;
1247
+ }
1248
+ onChange?.({ ...value, [k]: v });
1249
+ setKey("");
1250
+ setVal("");
1251
+ setProblem(null);
1252
+ };
1253
+ const remove = (k) => {
1254
+ const { [k]: _removed, ...rest } = value;
1255
+ onChange?.(rest);
1256
+ };
1257
+ const replace = (from, k, v) => {
1258
+ const nextKey = k.trim();
1259
+ if (!nextKey) {
1260
+ setProblem("Enter a key.");
1261
+ return false;
1262
+ }
1263
+ if (nextKey !== from && nextKey in value) {
1264
+ setProblem(`The key '${nextKey}' is taken.`);
1265
+ return false;
1266
+ }
1267
+ setProblem(null);
1268
+ if (nextKey === from && value[from] === v) return true;
1269
+ onChange?.(
1270
+ Object.fromEntries(
1271
+ Object.entries(value).map(
1272
+ ([key0, v0]) => key0 === from ? [nextKey, v] : [key0, v0]
1273
+ )
1274
+ )
1275
+ );
1276
+ return true;
1277
+ };
1278
+ const edit = (k) => {
1279
+ setKey(k);
1280
+ setVal(value[k] ?? "");
1281
+ setProblem(null);
1282
+ };
1283
+ const onKeyDown = (e) => {
1284
+ if (e.key !== "Enter") return;
1285
+ e.preventDefault();
1286
+ add();
1287
+ };
1288
+ const typeKey = (k) => {
1289
+ setKey(k);
1290
+ setProblem(null);
1291
+ };
1292
+ const typeValue = (v) => {
1293
+ setVal(v);
1294
+ setProblem(null);
1295
+ };
1296
+ return {
1297
+ key,
1298
+ typeKey,
1299
+ val,
1300
+ typeValue,
1301
+ problem,
1302
+ add,
1303
+ remove,
1304
+ replace,
1305
+ edit,
1306
+ onKeyDown
1307
+ };
1308
+ }
1309
+ function PairTags({
1310
+ value,
1311
+ separator,
1312
+ isDisabled,
1313
+ editor
1314
+ }) {
1315
+ const keys = Object.keys(value);
1316
+ if (keys.length === 0) return null;
1317
+ return /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap gap-1.5", children: keys.map((k, index) => /* @__PURE__ */ jsx10(
1318
+ TagBadge,
1319
+ {
1320
+ tag: `${k}${separator}${value[k]}`,
1321
+ index,
1322
+ isDisabled,
1323
+ onRemove: (i) => editor.remove(keys[i]),
1324
+ onSelect: (i) => editor.edit(keys[i])
1325
+ },
1326
+ k
1327
+ )) });
1328
+ }
1329
+ function ValueInput({
1330
+ ariaLabel,
1331
+ value,
1332
+ onType,
1333
+ onPick,
1334
+ onBlur,
1335
+ onKeyDown,
1336
+ placeholder,
1337
+ readOnly,
1338
+ suggestions,
1339
+ className
1340
+ }) {
1341
+ if (!suggestions?.length)
1342
+ return /* @__PURE__ */ jsx10(
1343
+ Input4,
1344
+ {
1345
+ "aria-label": ariaLabel,
1346
+ value,
1347
+ onChange: (e) => onType(e.target.value),
1348
+ onBlur,
1349
+ onKeyDown,
1350
+ placeholder,
1351
+ readOnly,
1352
+ className
1353
+ }
1354
+ );
1355
+ const options = suggestions.map((s) => ({
1356
+ value: s.value,
1357
+ label: s.label ?? s.value
1358
+ }));
1359
+ if (value && !options.some((o) => o.value === value))
1360
+ options.unshift({ value, label: value });
1361
+ return /* @__PURE__ */ jsx10(
1362
+ FormSelect,
1363
+ {
1364
+ label: ariaLabel,
1365
+ compact: true,
1366
+ options,
1367
+ value: value || null,
1368
+ onChange: (picked) => onPick(picked ?? ""),
1369
+ allowCreate: true,
1370
+ onCreate: onPick,
1371
+ placeholder,
1372
+ searchPlaceholder: "Search, or type a value",
1373
+ readOnly,
1374
+ wrapperClassName: className
1375
+ }
1376
+ );
1377
+ }
1378
+ function PairRow({
1379
+ pairKey,
1380
+ pairValue,
1381
+ isDisabled,
1382
+ editor,
1383
+ suggestions
1384
+ }) {
1385
+ const [key, setKey] = React8.useState(pairKey);
1386
+ const [val, setVal] = React8.useState(pairValue);
1387
+ React8.useEffect(() => setKey(pairKey), [pairKey]);
1388
+ React8.useEffect(() => setVal(pairValue), [pairValue]);
1389
+ const commit = () => {
1390
+ if (!editor.replace(pairKey, key, val)) setKey(pairKey);
1391
+ };
1392
+ const onKeyDown = (e) => {
1393
+ if (e.key !== "Enter") return;
1394
+ e.preventDefault();
1395
+ commit();
1396
+ };
1397
+ return /* @__PURE__ */ jsxs9("li", { className: "flex items-center gap-2", children: [
1398
+ /* @__PURE__ */ jsx10(
1399
+ Input4,
1400
+ {
1401
+ "aria-label": `Key of ${pairKey}`,
1402
+ value: key,
1403
+ onChange: (e) => setKey(e.target.value),
1404
+ onBlur: commit,
1405
+ onKeyDown,
1406
+ readOnly: isDisabled,
1407
+ className: "h-8 w-1/3 shrink-0 font-mono text-xs"
1408
+ }
1409
+ ),
1410
+ /* @__PURE__ */ jsx10(
1411
+ ValueInput,
1412
+ {
1413
+ ariaLabel: `Value of ${pairKey}`,
1414
+ value: val,
1415
+ onType: setVal,
1416
+ onPick: (picked) => {
1417
+ setVal(picked);
1418
+ if (!editor.replace(pairKey, key, picked)) setVal(pairValue);
1419
+ },
1420
+ onBlur: commit,
1421
+ onKeyDown,
1422
+ readOnly: isDisabled,
1423
+ suggestions,
1424
+ className: "h-8 min-w-0 flex-1 font-mono text-xs"
1425
+ }
1426
+ ),
1427
+ !isDisabled && /* @__PURE__ */ jsx10(
1428
+ IconButton,
1429
+ {
1430
+ label: `Remove ${pairKey}`,
1431
+ tone: "destructive",
1432
+ onClick: () => editor.remove(pairKey),
1433
+ children: /* @__PURE__ */ jsx10(X4, {})
1434
+ }
1435
+ )
1436
+ ] });
1437
+ }
1438
+ function PairRows({
1439
+ value,
1440
+ isDisabled,
1441
+ editor,
1442
+ suggestions
1443
+ }) {
1444
+ const keys = Object.keys(value);
1445
+ if (keys.length === 0) return null;
1446
+ return /* @__PURE__ */ jsx10("ul", { className: "space-y-1.5", children: keys.map((k, index) => /* @__PURE__ */ jsx10(
1447
+ PairRow,
1448
+ {
1449
+ pairKey: k,
1450
+ pairValue: value[k],
1451
+ isDisabled,
1452
+ editor,
1453
+ suggestions
1454
+ },
1455
+ index
1456
+ )) });
1457
+ }
1458
+ function PairKeys({
1459
+ value
1460
+ }) {
1461
+ const keys = Object.keys(value);
1462
+ return /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap gap-1.5", children: keys.map((k) => /* @__PURE__ */ jsx10(
1463
+ Badge3,
1464
+ {
1465
+ variant: "outline",
1466
+ className: "font-mono text-[11px] font-normal",
1467
+ title: value[k],
1468
+ children: k
1469
+ },
1470
+ k
1471
+ )) });
1472
+ }
1473
+ function PairList({
1474
+ display,
1475
+ suggestions,
1476
+ ...props
1477
+ }) {
1478
+ return display === "rows" ? /* @__PURE__ */ jsx10(PairRows, { ...props, suggestions }) : /* @__PURE__ */ jsx10(PairTags, { ...props });
1479
+ }
1480
+ var PairFields = React8.forwardRef(
1481
+ ({
1482
+ id,
1483
+ name,
1484
+ keyPlaceholder = "Key",
1485
+ valuePlaceholder = "Value",
1486
+ asRow = false,
1487
+ editor,
1488
+ suggestions
1489
+ }, ref) => /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
1490
+ /* @__PURE__ */ jsx10(
1491
+ Input4,
1492
+ {
1493
+ ref,
1494
+ id,
1495
+ name,
1496
+ value: editor.key,
1497
+ onChange: (e) => editor.typeKey(e.target.value),
1498
+ onKeyDown: editor.onKeyDown,
1499
+ placeholder: keyPlaceholder,
1500
+ className: cn9("h-8", asRow && "w-1/3 shrink-0 font-mono text-xs")
1501
+ }
1502
+ ),
1503
+ /* @__PURE__ */ jsx10(
1504
+ ValueInput,
1505
+ {
1506
+ ariaLabel: valuePlaceholder,
1507
+ value: editor.val,
1508
+ onType: editor.typeValue,
1509
+ onPick: editor.typeValue,
1510
+ onKeyDown: editor.onKeyDown,
1511
+ placeholder: valuePlaceholder,
1512
+ suggestions,
1513
+ className: cn9("h-8", asRow && "min-w-0 flex-1 font-mono text-xs")
1514
+ }
1515
+ ),
1516
+ /* @__PURE__ */ jsx10(
1517
+ Button3,
1518
+ {
1519
+ type: "button",
1520
+ variant: "outline",
1521
+ size: "compact",
1522
+ "aria-label": "Add",
1523
+ onClick: editor.add,
1524
+ children: /* @__PURE__ */ jsx10(Plus2, { className: "h-4 w-4" })
1525
+ }
1526
+ )
1527
+ ] })
1528
+ );
1529
+ PairFields.displayName = "PairFields";
1530
+ function PairBox({
1531
+ isDisabled,
1532
+ hasError,
1533
+ children
1534
+ }) {
1535
+ return /* @__PURE__ */ jsx10(
1536
+ "div",
1537
+ {
1538
+ className: cn9(
1539
+ "w-full space-y-2 rounded-md border border-input bg-background px-3 py-2 text-sm",
1540
+ isDisabled && "cursor-not-allowed opacity-50",
1541
+ hasError && "border-destructive"
1542
+ ),
1543
+ children
1544
+ }
1545
+ );
1546
+ }
1547
+ var PairEditorBox = React8.forwardRef(
1548
+ ({
1549
+ display,
1550
+ value,
1551
+ separator,
1552
+ isDisabled,
1553
+ hasError,
1554
+ editor,
1555
+ suggestions,
1556
+ ...fields
1557
+ }, ref) => /* @__PURE__ */ jsxs9(PairBox, { isDisabled, hasError, children: [
1558
+ /* @__PURE__ */ jsx10(
1559
+ PairList,
1560
+ {
1561
+ display,
1562
+ value,
1563
+ separator,
1564
+ isDisabled,
1565
+ editor,
1566
+ suggestions
1567
+ }
1568
+ ),
1569
+ !isDisabled && /* @__PURE__ */ jsx10(
1570
+ PairFields,
1571
+ {
1572
+ ref,
1573
+ ...fields,
1574
+ asRow: display === "rows",
1575
+ editor,
1576
+ suggestions
1577
+ }
1578
+ )
1579
+ ] })
1580
+ );
1581
+ PairEditorBox.displayName = "PairEditorBox";
1582
+ var FormKeyValueInput = React8.forwardRef((props, ref) => {
1583
+ const {
1584
+ label,
1585
+ description,
1586
+ error,
1587
+ wrapperClassName,
1588
+ labelPlacement,
1589
+ id,
1590
+ value = {},
1591
+ onChange,
1592
+ separator = ":",
1593
+ display = "tags"
1594
+ } = props;
1595
+ const generatedId = React8.useId();
1596
+ const fieldId = id ?? generatedId;
1597
+ const editor = usePairEditor(value, onChange);
1598
+ return /* @__PURE__ */ jsx10(
1599
+ FormFieldWrapper,
1600
+ {
1601
+ inputId: fieldId,
1602
+ label: resolveFieldLabel(label),
1603
+ description,
1604
+ error: error ?? editor.problem ?? void 0,
1605
+ required: props.required ?? false,
1606
+ labelPlacement,
1607
+ className: wrapperClassName,
1608
+ children: display === "keys" ? /* @__PURE__ */ jsx10(PairKeys, { value }) : /* @__PURE__ */ jsx10(
1609
+ PairEditorBox,
1610
+ {
1611
+ ref,
1612
+ display,
1613
+ value,
1614
+ separator,
1615
+ isDisabled: Boolean(props.disabled || props.readOnly),
1616
+ hasError: Boolean(error),
1617
+ id: fieldId,
1618
+ name: props.name,
1619
+ keyPlaceholder: props.keyPlaceholder,
1620
+ valuePlaceholder: props.valuePlaceholder,
1621
+ editor,
1622
+ suggestions: props.valueSuggestions
1623
+ }
1624
+ )
1625
+ }
1626
+ );
1627
+ });
1628
+ FormKeyValueInput.displayName = "FormKeyValueInput";
1629
+
1201
1630
  // src/FormQuantityInput.tsx
1202
- import React8, { useCallback as useCallback2, useEffect as useEffect2, useId as useId8, useMemo, useState as useState4 } from "react";
1631
+ import React9, { useCallback as useCallback2, useEffect as useEffect3, useId as useId9, useMemo, useState as useState5 } from "react";
1203
1632
  import { ChevronsUpDown as ChevronsUpDown3 } from "lucide-react";
1204
- import { cn as cn9 } from "@petrarca/sonnet-core";
1633
+ import { cn as cn10 } from "@petrarca/sonnet-core";
1205
1634
  import {
1206
1635
  InputGroup,
1207
1636
  InputGroupAddon,
1208
1637
  InputGroupInput
1209
1638
  } from "@petrarca/sonnet-ui";
1210
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1639
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1211
1640
  var VALID_NUMERIC_INPUT = /^-?\d*[.,]?\d*$/;
1212
1641
  var INCOMPLETE_SENTINELS = /* @__PURE__ */ new Set(["", ".", "-", "-."]);
1213
1642
  function roundToPrecision(n, p) {
@@ -1228,7 +1657,31 @@ function roundAndFormat(numValue, precision) {
1228
1657
  const formatted = precision !== void 0 ? rounded.toFixed(precision) : String(rounded);
1229
1658
  return { rounded, formatted };
1230
1659
  }
1231
- function UnitAddon({
1660
+ function FreeUnitAddon({
1661
+ currentUnit,
1662
+ addonAlign,
1663
+ disabled,
1664
+ readOnly,
1665
+ onChange
1666
+ }) {
1667
+ return /* @__PURE__ */ jsx11(InputGroupAddon, { align: addonAlign, children: /* @__PURE__ */ jsx11(
1668
+ "input",
1669
+ {
1670
+ value: currentUnit,
1671
+ onChange,
1672
+ disabled,
1673
+ readOnly,
1674
+ placeholder: "unit",
1675
+ "aria-label": "Unit",
1676
+ className: "w-16 border-0 bg-transparent px-1 text-right text-sm text-muted-foreground outline-none placeholder:text-muted-foreground/60 focus:text-foreground"
1677
+ }
1678
+ ) });
1679
+ }
1680
+ function UnitAddon({ units, freeUnit, ...rest }) {
1681
+ if (units.length === 0) return freeUnit ? /* @__PURE__ */ jsx11(FreeUnitAddon, { ...rest }) : null;
1682
+ return /* @__PURE__ */ jsx11(UnitPicker, { units, ...rest });
1683
+ }
1684
+ function UnitPicker({
1232
1685
  units,
1233
1686
  currentUnit,
1234
1687
  addonAlign,
@@ -1236,12 +1689,11 @@ function UnitAddon({
1236
1689
  readOnly,
1237
1690
  onChange
1238
1691
  }) {
1239
- if (units.length === 0) return null;
1240
1692
  if (units.length === 1) {
1241
- return /* @__PURE__ */ jsx9(InputGroupAddon, { align: addonAlign, children: /* @__PURE__ */ jsx9("span", { className: "text-sm text-muted-foreground", children: units[0].label }) });
1693
+ return /* @__PURE__ */ jsx11(InputGroupAddon, { align: addonAlign, children: /* @__PURE__ */ jsx11("span", { className: "text-sm text-muted-foreground", children: units[0].label }) });
1242
1694
  }
1243
- return /* @__PURE__ */ jsx9(InputGroupAddon, { align: addonAlign, children: /* @__PURE__ */ jsxs8("div", { className: "relative flex items-center", children: [
1244
- /* @__PURE__ */ jsx9(
1695
+ return /* @__PURE__ */ jsx11(InputGroupAddon, { align: addonAlign, children: /* @__PURE__ */ jsxs10("div", { className: "relative flex items-center", children: [
1696
+ /* @__PURE__ */ jsx11(
1245
1697
  "select",
1246
1698
  {
1247
1699
  value: currentUnit,
@@ -1249,15 +1701,16 @@ function UnitAddon({
1249
1701
  disabled: disabled || readOnly,
1250
1702
  className: "cursor-pointer appearance-none border-0 bg-transparent py-0 pr-5 pl-1 text-sm text-muted-foreground outline-none hover:text-foreground focus:text-foreground",
1251
1703
  "aria-label": "Unit",
1252
- children: units.map((u) => /* @__PURE__ */ jsx9("option", { value: u.value, children: u.label }, u.value))
1704
+ children: units.map((u) => /* @__PURE__ */ jsx11("option", { value: u.value, children: u.label }, u.value))
1253
1705
  }
1254
1706
  ),
1255
- /* @__PURE__ */ jsx9(ChevronsUpDown3, { className: "pointer-events-none absolute right-0 h-3.5 w-3.5 text-muted-foreground/60" })
1707
+ /* @__PURE__ */ jsx11(ChevronsUpDown3, { className: "pointer-events-none absolute right-0 h-3.5 w-3.5 text-muted-foreground/60" })
1256
1708
  ] }) });
1257
1709
  }
1258
1710
  function useUnitState(value, units, unitPosition, min, max) {
1259
1711
  const currentUnit = useMemo(() => {
1260
1712
  const preferred = value?.unit;
1713
+ if (units.length === 0) return preferred ?? "";
1261
1714
  if (preferred && units.some((u) => u.value === preferred)) return preferred;
1262
1715
  return units[0]?.value ?? "";
1263
1716
  }, [value?.unit, units]);
@@ -1272,11 +1725,11 @@ function useUnitState(value, units, unitPosition, min, max) {
1272
1725
  return { currentUnit, effectiveMin, effectiveMax, addonAlign };
1273
1726
  }
1274
1727
  function useQuantityInput(value, onChange, onBlur, currentUnit, precision, effectiveMin, effectiveMax, externalError) {
1275
- const [rawInput, setRawInput] = useState4(
1728
+ const [rawInput, setRawInput] = useState5(
1276
1729
  value?.value !== void 0 ? String(value.value) : ""
1277
1730
  );
1278
- const [validationError, setValidationError] = useState4("");
1279
- useEffect2(() => {
1731
+ const [validationError, setValidationError] = useState5("");
1732
+ useEffect3(() => {
1280
1733
  const external = value?.value !== void 0 ? String(value.value) : "";
1281
1734
  setRawInput((prev) => {
1282
1735
  const parsed = parseFloat(prev.replace(",", "."));
@@ -1341,7 +1794,7 @@ function useQuantityInput(value, onChange, onBlur, currentUnit, precision, effec
1341
1794
  setValidationError
1342
1795
  };
1343
1796
  }
1344
- var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1797
+ var FormQuantityInput = React9.forwardRef((allProps, ref) => {
1345
1798
  const {
1346
1799
  value,
1347
1800
  onChange,
@@ -1360,12 +1813,13 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1360
1813
  wrapperClassName,
1361
1814
  labelPlacement,
1362
1815
  className,
1363
- name
1816
+ name,
1817
+ freeUnit
1364
1818
  } = allProps;
1365
1819
  const units = useMemo(() => allProps.units ?? [], [allProps.units]);
1366
1820
  const unitPosition = allProps.unitPosition ?? "suffix";
1367
1821
  const error = allProps.error;
1368
- const generatedId = useId8();
1822
+ const generatedId = useId9();
1369
1823
  const inputId = allProps.id ?? generatedId;
1370
1824
  const { currentUnit, effectiveMin, effectiveMax, addonAlign } = useUnitState(
1371
1825
  value,
@@ -1397,7 +1851,7 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1397
1851
  setValidationError("");
1398
1852
  const normalized = rawInput.replace(",", ".");
1399
1853
  const numValue = parseFloat(normalized);
1400
- if (isNaN(numValue)) return;
1854
+ if (isNaN(numValue)) return emit(void 0, unitValue);
1401
1855
  const um = units.find((u) => u.value === unitValue);
1402
1856
  const lo = um?.min ?? min;
1403
1857
  const hi = um?.max ?? max;
@@ -1407,10 +1861,11 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1407
1861
  [rawInput, units, min, max, emit, setValidationError]
1408
1862
  );
1409
1863
  const unitAddon = useMemo(
1410
- () => /* @__PURE__ */ jsx9(
1864
+ () => /* @__PURE__ */ jsx11(
1411
1865
  UnitAddon,
1412
1866
  {
1413
1867
  units,
1868
+ freeUnit,
1414
1869
  currentUnit,
1415
1870
  addonAlign,
1416
1871
  disabled,
@@ -1418,9 +1873,17 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1418
1873
  onChange: handleUnitChange
1419
1874
  }
1420
1875
  ),
1421
- [units, currentUnit, addonAlign, disabled, readOnly, handleUnitChange]
1876
+ [
1877
+ units,
1878
+ freeUnit,
1879
+ currentUnit,
1880
+ addonAlign,
1881
+ disabled,
1882
+ readOnly,
1883
+ handleUnitChange
1884
+ ]
1422
1885
  );
1423
- return /* @__PURE__ */ jsx9(
1886
+ return /* @__PURE__ */ jsx11(
1424
1887
  FormFieldWrapper,
1425
1888
  {
1426
1889
  inputId,
@@ -1430,13 +1893,13 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1430
1893
  required,
1431
1894
  labelPlacement,
1432
1895
  className: wrapperClassName,
1433
- children: /* @__PURE__ */ jsxs8(
1896
+ children: /* @__PURE__ */ jsxs10(
1434
1897
  InputGroup,
1435
1898
  {
1436
1899
  "data-disabled": disabled || void 0,
1437
- className: cn9(resolvedError && "border-destructive"),
1900
+ className: cn10(resolvedError && "border-destructive"),
1438
1901
  children: [
1439
- /* @__PURE__ */ jsx9(
1902
+ /* @__PURE__ */ jsx11(
1440
1903
  InputGroupInput,
1441
1904
  {
1442
1905
  id: inputId,
@@ -1474,9 +1937,13 @@ var FormQuantityInput = React8.forwardRef((allProps, ref) => {
1474
1937
  FormQuantityInput.displayName = "FormQuantityInput";
1475
1938
 
1476
1939
  // src/FormActions.tsx
1477
- import { Button as Button3 } from "@petrarca/sonnet-ui";
1940
+ import { Button as Button4 } from "@petrarca/sonnet-ui";
1478
1941
  import { SimpleGroup } from "@petrarca/sonnet-ui";
1479
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1942
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1943
+ function isSaveDisabled(args) {
1944
+ if (args.disabled || !args.hasChanges || args.isValidating) return true;
1945
+ return args.saveWhenInvalid === "disable" && !args.isValid;
1946
+ }
1480
1947
  function FormActions({
1481
1948
  onSave,
1482
1949
  onCancel,
@@ -1487,14 +1954,39 @@ function FormActions({
1487
1954
  saveLabel = "Save",
1488
1955
  cancelLabel = "Cancel",
1489
1956
  showCancel = true,
1957
+ isValidating = false,
1490
1958
  leadingActions
1491
1959
  }) {
1492
- const saveDisabled = disabled || !hasChanges || saveWhenInvalid === "disable" && !isValid;
1493
- return /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between mt-4", children: [
1494
- /* @__PURE__ */ jsx10("div", { className: "flex items-center gap-2", children: leadingActions }),
1495
- /* @__PURE__ */ jsxs9(SimpleGroup, { justify: "flex-end", children: [
1496
- showCancel && /* @__PURE__ */ jsx10(Button3, { variant: "outline", onClick: onCancel, disabled, children: cancelLabel }),
1497
- /* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: saveDisabled, children: saveLabel })
1960
+ const saveDisabled = isSaveDisabled({
1961
+ disabled,
1962
+ hasChanges,
1963
+ isValid,
1964
+ isValidating,
1965
+ saveWhenInvalid
1966
+ });
1967
+ const keepFocus = (e) => e.preventDefault();
1968
+ return /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between mt-4", children: [
1969
+ /* @__PURE__ */ jsx12("div", { className: "flex items-center gap-2", children: leadingActions }),
1970
+ /* @__PURE__ */ jsxs11(SimpleGroup, { justify: "flex-end", children: [
1971
+ showCancel && /* @__PURE__ */ jsx12(
1972
+ Button4,
1973
+ {
1974
+ variant: "outline",
1975
+ onClick: onCancel,
1976
+ onMouseDown: keepFocus,
1977
+ disabled,
1978
+ children: cancelLabel
1979
+ }
1980
+ ),
1981
+ /* @__PURE__ */ jsx12(
1982
+ Button4,
1983
+ {
1984
+ onClick: onSave,
1985
+ onMouseDown: keepFocus,
1986
+ disabled: saveDisabled,
1987
+ children: saveLabel
1988
+ }
1989
+ )
1498
1990
  ] })
1499
1991
  ] });
1500
1992
  }
@@ -1505,7 +1997,7 @@ import { ChevronDown, ChevronRight } from "lucide-react";
1505
1997
  import { Card } from "@petrarca/sonnet-ui";
1506
1998
  import { useDisclosure } from "@petrarca/sonnet-core/hooks";
1507
1999
  import { Collapsible, CollapsibleContent } from "@petrarca/sonnet-ui";
1508
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2000
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1509
2001
  function ExtraPropertiesCard({ properties, schema }) {
1510
2002
  const [opened, { toggle }] = useDisclosure(false);
1511
2003
  const extraProperties = useMemo2(() => {
@@ -1517,32 +2009,35 @@ function ExtraPropertiesCard({ properties, schema }) {
1517
2009
  return Object.keys(extra).length > 0 ? extra : null;
1518
2010
  }, [properties, schema]);
1519
2011
  if (!extraProperties) return null;
1520
- return /* @__PURE__ */ jsxs10(Card, { className: "border p-3", children: [
1521
- /* @__PURE__ */ jsxs10(
2012
+ return /* @__PURE__ */ jsxs12(Card, { className: "border p-3", children: [
2013
+ /* @__PURE__ */ jsxs12(
1522
2014
  "p",
1523
2015
  {
1524
2016
  className: `flex items-center gap-1 text-sm font-medium cursor-pointer select-none ${opened ? "mb-1" : ""}`,
1525
2017
  onClick: toggle,
1526
2018
  children: [
1527
- opened ? /* @__PURE__ */ jsx11(ChevronDown, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx11(ChevronRight, { className: "h-3.5 w-3.5" }),
2019
+ opened ? /* @__PURE__ */ jsx13(ChevronDown, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx13(ChevronRight, { className: "h-3.5 w-3.5" }),
1528
2020
  "Extra Properties (not in schema)"
1529
2021
  ]
1530
2022
  }
1531
2023
  ),
1532
- /* @__PURE__ */ jsx11(Collapsible, { open: opened, children: /* @__PURE__ */ jsx11(CollapsibleContent, { children: /* @__PURE__ */ jsx11("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx11("code", { children: JSON.stringify(extraProperties, null, 2) }) }) }) })
2024
+ /* @__PURE__ */ jsx13(Collapsible, { open: opened, children: /* @__PURE__ */ jsx13(CollapsibleContent, { children: /* @__PURE__ */ jsx13("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx13("code", { children: JSON.stringify(extraProperties, null, 2) }) }) }) })
1533
2025
  ] });
1534
2026
  }
1535
2027
  var ExtraPropertiesCard_default = ExtraPropertiesCard;
1536
2028
 
1537
2029
  // src/JsonSchemaFormRenderer.tsx
1538
- import React13, { useCallback as useCallback8, useContext as useContext2, useMemo as useMemo8 } from "react";
2030
+ import React18, { useCallback as useCallback9, useContext as useContext2, useMemo as useMemo12 } from "react";
1539
2031
  import { SimpleStack } from "@petrarca/sonnet-ui";
1540
2032
  import { AlertCircle as AlertCircle2 } from "lucide-react";
1541
2033
  import {
1542
2034
  WIDTH_TOKEN_REMS
1543
2035
  } from "@petrarca/sonnet-core/schema";
1544
- import { isFieldRequired, evaluateUiRule } from "@petrarca/sonnet-core/schema";
1545
- import { FORM_ITEM_ID_FIELD as FORM_ITEM_ID_FIELD2 } from "@petrarca/sonnet-core";
2036
+ import {
2037
+ isFieldRequired,
2038
+ evaluateUiRule,
2039
+ stripFormItemIds as stripFormItemIds2
2040
+ } from "@petrarca/sonnet-core/schema";
1546
2041
 
1547
2042
  // src/widgets/types.ts
1548
2043
  function firstError(rawErrors) {
@@ -1550,7 +2045,7 @@ function firstError(rawErrors) {
1550
2045
  }
1551
2046
 
1552
2047
  // src/widgets/TextInputWidget.tsx
1553
- import { jsx as jsx12 } from "react/jsx-runtime";
2048
+ import { jsx as jsx14 } from "react/jsx-runtime";
1554
2049
  function inputTypeFromFormat(format) {
1555
2050
  if (format === "email") return "email";
1556
2051
  if (format === "uri" || format === "url") return "url";
@@ -1575,7 +2070,7 @@ function TextInputWidget({
1575
2070
  }) {
1576
2071
  const error = firstError(rawErrors);
1577
2072
  const compact = options?.compact;
1578
- return /* @__PURE__ */ jsx12(
2073
+ return /* @__PURE__ */ jsx14(
1579
2074
  FormInput,
1580
2075
  {
1581
2076
  id,
@@ -1602,7 +2097,7 @@ function TextInputWidget({
1602
2097
  }
1603
2098
 
1604
2099
  // src/widgets/TextareaWidget.tsx
1605
- import { jsx as jsx13 } from "react/jsx-runtime";
2100
+ import { jsx as jsx15 } from "react/jsx-runtime";
1606
2101
  function TextareaWidget({
1607
2102
  id,
1608
2103
  name,
@@ -1623,7 +2118,7 @@ function TextareaWidget({
1623
2118
  const minRows = options.rows || options.minRows || 3;
1624
2119
  const autosize = options.autosize !== false;
1625
2120
  const error = firstError(rawErrors);
1626
- return /* @__PURE__ */ jsx13(
2121
+ return /* @__PURE__ */ jsx15(
1627
2122
  FormTextarea,
1628
2123
  {
1629
2124
  id,
@@ -1649,7 +2144,7 @@ function TextareaWidget({
1649
2144
  }
1650
2145
 
1651
2146
  // src/widgets/NumberWidget.tsx
1652
- import { jsx as jsx14 } from "react/jsx-runtime";
2147
+ import { jsx as jsx16 } from "react/jsx-runtime";
1653
2148
  function NumberWidget({
1654
2149
  id,
1655
2150
  name,
@@ -1671,7 +2166,7 @@ function NumberWidget({
1671
2166
  const step = options.step ?? (isInteger ? 1 : void 0);
1672
2167
  const allowDecimal = options.allowDecimal ?? !isInteger;
1673
2168
  const error = firstError(rawErrors);
1674
- return /* @__PURE__ */ jsx14(
2169
+ return /* @__PURE__ */ jsx16(
1675
2170
  FormNumberInput,
1676
2171
  {
1677
2172
  id,
@@ -1699,7 +2194,7 @@ function NumberWidget({
1699
2194
  // src/widgets/CheckboxWidget.tsx
1700
2195
  import { useCallback as useCallback3 } from "react";
1701
2196
  import { isNullableBoolean } from "@petrarca/sonnet-core/schema";
1702
- import { jsx as jsx15 } from "react/jsx-runtime";
2197
+ import { jsx as jsx17 } from "react/jsx-runtime";
1703
2198
  function CheckboxWidget({
1704
2199
  id,
1705
2200
  value,
@@ -1718,12 +2213,12 @@ function CheckboxWidget({
1718
2213
  const compact = options?.compact;
1719
2214
  const isTriState = isNullableBoolean(propertySchema);
1720
2215
  const isChecked = value === true;
1721
- const isIndeterminate = isTriState && value === null;
2216
+ const isIndeterminate = isTriState && value == null;
1722
2217
  const checkedState = isIndeterminate ? "indeterminate" : isChecked;
1723
2218
  const handleChange = useCallback3(
1724
2219
  (_newChecked) => {
1725
2220
  if (isTriState) {
1726
- if (value === null) {
2221
+ if (value == null) {
1727
2222
  onChange(true);
1728
2223
  } else if (value === true) {
1729
2224
  onChange(false);
@@ -1736,7 +2231,7 @@ function CheckboxWidget({
1736
2231
  },
1737
2232
  [isTriState, value, onChange]
1738
2233
  );
1739
- return /* @__PURE__ */ jsx15(
2234
+ return /* @__PURE__ */ jsx17(
1740
2235
  FormCheckbox,
1741
2236
  {
1742
2237
  id,
@@ -1756,15 +2251,7 @@ function CheckboxWidget({
1756
2251
 
1757
2252
  // src/widgets/SelectWidget.tsx
1758
2253
  import { useRef as useRef3 } from "react";
1759
- import { extractEnumValues } from "@petrarca/sonnet-core/schema";
1760
- import { jsx as jsx16 } from "react/jsx-runtime";
1761
- function buildSelectOptions(propertySchema, enumNames) {
1762
- const enumValues = extractEnumValues(propertySchema);
1763
- return enumValues.map((val) => {
1764
- const key = String(val);
1765
- return { value: key, label: enumNames[key] || key };
1766
- });
1767
- }
2254
+ import { jsx as jsx18 } from "react/jsx-runtime";
1768
2255
  function normalizeMultiValue(value) {
1769
2256
  if (Array.isArray(value)) return value.map(String);
1770
2257
  if (value) return [String(value)];
@@ -1804,7 +2291,7 @@ function SelectWidget(props) {
1804
2291
  const triggerClassName = compact ? "h-8 text-xs" : void 0;
1805
2292
  const handleBlur = () => onBlur?.(id, valueRef.current);
1806
2293
  if (isMultiple) {
1807
- return /* @__PURE__ */ jsx16(
2294
+ return /* @__PURE__ */ jsx18(
1808
2295
  FormMultiSelect,
1809
2296
  {
1810
2297
  id,
@@ -1828,7 +2315,7 @@ function SelectWidget(props) {
1828
2315
  }
1829
2316
  );
1830
2317
  }
1831
- return /* @__PURE__ */ jsx16(
2318
+ return /* @__PURE__ */ jsx18(
1832
2319
  FormSelect,
1833
2320
  {
1834
2321
  id,
@@ -1856,7 +2343,7 @@ function SelectWidget(props) {
1856
2343
  }
1857
2344
 
1858
2345
  // src/widgets/TagsInputWidget.tsx
1859
- import { jsx as jsx17 } from "react/jsx-runtime";
2346
+ import { jsx as jsx19 } from "react/jsx-runtime";
1860
2347
  function TagsInputWidget({
1861
2348
  id,
1862
2349
  name,
@@ -1873,7 +2360,7 @@ function TagsInputWidget({
1873
2360
  }) {
1874
2361
  const error = firstError(rawErrors);
1875
2362
  const tags = Array.isArray(value) ? value.map(String) : [];
1876
- return /* @__PURE__ */ jsx17(
2363
+ return /* @__PURE__ */ jsx19(
1877
2364
  FormTagsInput,
1878
2365
  {
1879
2366
  id,
@@ -1896,9 +2383,13 @@ function TagsInputWidget({
1896
2383
  }
1897
2384
 
1898
2385
  // src/widgets/JsonEditorWidget.tsx
1899
- import { useCallback as useCallback4, useState as useState5 } from "react";
1900
- import { JsonEditor } from "@petrarca/sonnet-ui/json-editor";
1901
- import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2386
+ import { Suspense, lazy, useCallback as useCallback4, useState as useState6 } from "react";
2387
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
2388
+ var JsonEditor = lazy(
2389
+ () => import("@petrarca/sonnet-ui/json-editor").then((m) => ({
2390
+ default: m.JsonEditor
2391
+ }))
2392
+ );
1902
2393
  var FULL_DOCUMENT = ["/"];
1903
2394
  function JsonEditorWidget({
1904
2395
  id,
@@ -1909,7 +2400,7 @@ function JsonEditorWidget({
1909
2400
  required,
1910
2401
  options
1911
2402
  }) {
1912
- const [raw, setRaw] = useState5(
2403
+ const [raw, setRaw] = useState6(
1913
2404
  value != null ? JSON.stringify(value, null, 2) : ""
1914
2405
  );
1915
2406
  const height = options?.height ?? "240px";
@@ -1924,42 +2415,104 @@ function JsonEditorWidget({
1924
2415
  },
1925
2416
  [onChange]
1926
2417
  );
1927
- return /* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
1928
- label && /* @__PURE__ */ jsxs11("label", { htmlFor: id, className: "text-sm font-medium leading-none", children: [
2418
+ return /* @__PURE__ */ jsxs13("div", { className: "space-y-2", children: [
2419
+ label && /* @__PURE__ */ jsxs13("label", { htmlFor: id, className: "text-sm font-medium leading-none", children: [
1929
2420
  label,
1930
- required && /* @__PURE__ */ jsx18("span", { className: "text-destructive ml-1", children: "*" })
2421
+ required && /* @__PURE__ */ jsx20("span", { className: "text-destructive ml-1", children: "*" })
1931
2422
  ] }),
1932
- description && /* @__PURE__ */ jsx18("p", { className: "text-sm text-muted-foreground", children: description }),
1933
- /* @__PURE__ */ jsx18(
1934
- JsonEditor,
2423
+ description && /* @__PURE__ */ jsx20("p", { className: "text-sm text-muted-foreground", children: description }),
2424
+ /* @__PURE__ */ jsx20(
2425
+ Suspense,
1935
2426
  {
1936
- value: raw,
1937
- editablePaths: FULL_DOCUMENT,
1938
- onChange: handleChange,
1939
- height,
1940
- className: "rounded-md border overflow-hidden"
2427
+ fallback: /* @__PURE__ */ jsx20(
2428
+ "div",
2429
+ {
2430
+ className: "rounded-md border bg-muted/30",
2431
+ style: { height },
2432
+ "aria-busy": true
2433
+ }
2434
+ ),
2435
+ children: /* @__PURE__ */ jsx20(
2436
+ JsonEditor,
2437
+ {
2438
+ value: raw,
2439
+ editablePaths: FULL_DOCUMENT,
2440
+ onChange: handleChange,
2441
+ height,
2442
+ className: "rounded-md border overflow-hidden"
2443
+ }
2444
+ )
1941
2445
  }
1942
2446
  )
1943
2447
  ] });
1944
2448
  }
1945
2449
 
1946
- // src/widgets/EntitySelectWidget.tsx
1947
- import { useMemo as useMemo3 } from "react";
1948
- import { EntitySelect } from "@petrarca/sonnet-ui";
1949
- import { useFetcherFactory } from "@petrarca/sonnet-core/entityOptions";
1950
- import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
1951
- var VALID_MODES = ["eager", "typeahead", "explicit"];
1952
- function resolveSelectMode(options) {
1953
- const rawMode = options?.mode;
1954
- if (typeof rawMode === "string" && VALID_MODES.includes(rawMode)) {
1955
- return rawMode;
1956
- }
1957
- return "eager";
2450
+ // src/widgets/KeyValueWidget.tsx
2451
+ import { jsx as jsx21 } from "react/jsx-runtime";
2452
+ function displayOf(display) {
2453
+ return display === "rows" || display === "keys" ? display : "tags";
1958
2454
  }
1959
- function resolveMinChars(options) {
1960
- const rawMinChars = options?.minChars;
1961
- if (typeof rawMinChars === "number" && rawMinChars >= 1) return rawMinChars;
1962
- return 1;
2455
+ function asStringMap(value) {
2456
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2457
+ return Object.fromEntries(
2458
+ Object.entries(value).map(([k, v]) => [
2459
+ k,
2460
+ String(v)
2461
+ ])
2462
+ );
2463
+ }
2464
+ function KeyValueWidget({
2465
+ id,
2466
+ name,
2467
+ value,
2468
+ onChange,
2469
+ required,
2470
+ disabled,
2471
+ readonly,
2472
+ rawErrors,
2473
+ label,
2474
+ description,
2475
+ options
2476
+ }) {
2477
+ return /* @__PURE__ */ jsx21(
2478
+ FormKeyValueInput,
2479
+ {
2480
+ id,
2481
+ name,
2482
+ value: asStringMap(value),
2483
+ onChange: (next) => onChange(next),
2484
+ label,
2485
+ labelPlacement: options?.labelPlacement,
2486
+ description,
2487
+ required,
2488
+ disabled,
2489
+ readOnly: readonly,
2490
+ error: firstError(rawErrors),
2491
+ keyPlaceholder: options?.keyPlaceholder,
2492
+ valuePlaceholder: options?.valuePlaceholder,
2493
+ separator: options?.separator,
2494
+ display: displayOf(options?.display)
2495
+ }
2496
+ );
2497
+ }
2498
+
2499
+ // src/widgets/EntitySelectWidget.tsx
2500
+ import { useMemo as useMemo3 } from "react";
2501
+ import { EntitySelect } from "@petrarca/sonnet-ui";
2502
+ import { useFetcherFactory } from "@petrarca/sonnet-core/entityOptions";
2503
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
2504
+ var VALID_MODES = ["eager", "typeahead", "explicit"];
2505
+ function resolveSelectMode(options) {
2506
+ const rawMode = options?.mode;
2507
+ if (typeof rawMode === "string" && VALID_MODES.includes(rawMode)) {
2508
+ return rawMode;
2509
+ }
2510
+ return "eager";
2511
+ }
2512
+ function resolveMinChars(options) {
2513
+ const rawMinChars = options?.minChars;
2514
+ if (typeof rawMinChars === "number" && rawMinChars >= 1) return rawMinChars;
2515
+ return 1;
1963
2516
  }
1964
2517
  function buildRestConfig(options) {
1965
2518
  if (!("endpoint" in options) || !options.endpoint) return null;
@@ -2019,13 +2572,13 @@ function EntitySelectWidget({
2019
2572
  [fetcherConfig, name]
2020
2573
  );
2021
2574
  if (!fetcher) {
2022
- return /* @__PURE__ */ jsxs12("div", { className: "text-sm text-destructive", children: [
2575
+ return /* @__PURE__ */ jsxs14("div", { className: "text-sm text-destructive", children: [
2023
2576
  'EntitySelectWidget: missing endpoint or query in x-ui-options for field "',
2024
2577
  name,
2025
2578
  '"'
2026
2579
  ] });
2027
2580
  }
2028
- return /* @__PURE__ */ jsx19(
2581
+ return /* @__PURE__ */ jsx22(
2029
2582
  EntitySelect,
2030
2583
  {
2031
2584
  id,
@@ -2071,7 +2624,7 @@ function useNestedFormContext() {
2071
2624
  }
2072
2625
 
2073
2626
  // src/templates/DefaultObjectContainerTemplate.tsx
2074
- import { useState as useState6 } from "react";
2627
+ import { useState as useState7 } from "react";
2075
2628
  import { ChevronDown as ChevronDown2, ChevronRight as ChevronRight2 } from "lucide-react";
2076
2629
  import { Card as Card2, CardContent, CardHeader, CardTitle } from "@petrarca/sonnet-ui";
2077
2630
  import {
@@ -2079,18 +2632,18 @@ import {
2079
2632
  CollapsibleContent as CollapsibleContent2,
2080
2633
  CollapsibleTrigger
2081
2634
  } from "@petrarca/sonnet-ui";
2082
- import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
2635
+ import { jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
2083
2636
  function CardVariant({
2084
2637
  children,
2085
2638
  label,
2086
2639
  description
2087
2640
  }) {
2088
- return /* @__PURE__ */ jsxs13(Card2, { children: [
2089
- label && /* @__PURE__ */ jsxs13(CardHeader, { className: "pb-3", children: [
2090
- /* @__PURE__ */ jsx20(CardTitle, { className: "text-base", children: label }),
2091
- description && /* @__PURE__ */ jsx20("p", { className: "text-sm text-muted-foreground", children: description })
2641
+ return /* @__PURE__ */ jsxs15(Card2, { children: [
2642
+ label && /* @__PURE__ */ jsxs15(CardHeader, { className: "pb-3", children: [
2643
+ /* @__PURE__ */ jsx23(CardTitle, { className: "text-base", children: label }),
2644
+ description && /* @__PURE__ */ jsx23("p", { className: "text-sm text-muted-foreground", children: description })
2092
2645
  ] }),
2093
- /* @__PURE__ */ jsx20(CardContent, { children })
2646
+ /* @__PURE__ */ jsx23(CardContent, { children })
2094
2647
  ] });
2095
2648
  }
2096
2649
  function SectionVariant({
@@ -2098,12 +2651,12 @@ function SectionVariant({
2098
2651
  label,
2099
2652
  description
2100
2653
  }) {
2101
- return /* @__PURE__ */ jsxs13("div", { className: "space-y-3", children: [
2102
- label && /* @__PURE__ */ jsxs13("div", { children: [
2103
- /* @__PURE__ */ jsx20("h4", { className: "text-sm font-medium", children: label }),
2104
- description && /* @__PURE__ */ jsx20("p", { className: "text-xs text-muted-foreground", children: description })
2654
+ return /* @__PURE__ */ jsxs15("div", { className: "space-y-3", children: [
2655
+ label && /* @__PURE__ */ jsxs15("div", { children: [
2656
+ /* @__PURE__ */ jsx23("h4", { className: "text-sm font-medium", children: label }),
2657
+ description && /* @__PURE__ */ jsx23("p", { className: "text-xs text-muted-foreground", children: description })
2105
2658
  ] }),
2106
- /* @__PURE__ */ jsx20("div", { className: "pl-3 border-l-2 border-muted", children })
2659
+ /* @__PURE__ */ jsx23("div", { className: "pl-3 border-l-2 border-muted", children })
2107
2660
  ] });
2108
2661
  }
2109
2662
  function DefaultObjectContainerTemplate({
@@ -2115,10 +2668,10 @@ function DefaultObjectContainerTemplate({
2115
2668
  }) {
2116
2669
  switch (variant) {
2117
2670
  case "card":
2118
- return /* @__PURE__ */ jsx20(CardVariant, { label, description, children });
2671
+ return /* @__PURE__ */ jsx23(CardVariant, { label, description, children });
2119
2672
  case "collapsible":
2120
- if (!label) return /* @__PURE__ */ jsx20(SectionVariant, { children });
2121
- return /* @__PURE__ */ jsx20(
2673
+ if (!label) return /* @__PURE__ */ jsx23(SectionVariant, { children });
2674
+ return /* @__PURE__ */ jsx23(
2122
2675
  CollapsibleVariant,
2123
2676
  {
2124
2677
  label,
@@ -2128,12 +2681,12 @@ function DefaultObjectContainerTemplate({
2128
2681
  }
2129
2682
  );
2130
2683
  case "inline":
2131
- return /* @__PURE__ */ jsx20("div", { children });
2684
+ return /* @__PURE__ */ jsx23("div", { children });
2132
2685
  case "borderless":
2133
- return /* @__PURE__ */ jsx20("div", { className: "space-y-4", children });
2686
+ return /* @__PURE__ */ jsx23("div", { className: "space-y-4", children });
2134
2687
  case "section":
2135
2688
  default:
2136
- return /* @__PURE__ */ jsx20(SectionVariant, { label, description, children });
2689
+ return /* @__PURE__ */ jsx23(SectionVariant, { label, description, children });
2137
2690
  }
2138
2691
  }
2139
2692
  function CollapsibleVariant({
@@ -2142,26 +2695,26 @@ function CollapsibleVariant({
2142
2695
  defaultOpen,
2143
2696
  children
2144
2697
  }) {
2145
- const [open, setOpen] = useState6(defaultOpen);
2146
- return /* @__PURE__ */ jsxs13(Collapsible2, { open, onOpenChange: setOpen, children: [
2147
- /* @__PURE__ */ jsx20(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsxs13(
2698
+ const [open, setOpen] = useState7(defaultOpen);
2699
+ return /* @__PURE__ */ jsxs15(Collapsible2, { open, onOpenChange: setOpen, children: [
2700
+ /* @__PURE__ */ jsx23(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsxs15(
2148
2701
  "button",
2149
2702
  {
2150
2703
  type: "button",
2151
2704
  className: "flex w-full items-center gap-2 text-sm font-medium hover:text-foreground/80",
2152
2705
  children: [
2153
- open ? /* @__PURE__ */ jsx20(ChevronDown2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx20(ChevronRight2, { className: "h-4 w-4" }),
2706
+ open ? /* @__PURE__ */ jsx23(ChevronDown2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx23(ChevronRight2, { className: "h-4 w-4" }),
2154
2707
  label
2155
2708
  ]
2156
2709
  }
2157
2710
  ) }),
2158
- description && /* @__PURE__ */ jsx20("p", { className: "text-xs text-muted-foreground ml-6", children: description }),
2159
- /* @__PURE__ */ jsx20(CollapsibleContent2, { className: "mt-2 pl-3 border-l-2 border-muted", children })
2711
+ description && /* @__PURE__ */ jsx23("p", { className: "text-xs text-muted-foreground ml-6", children: description }),
2712
+ /* @__PURE__ */ jsx23(CollapsibleContent2, { className: "mt-2 pl-3 border-l-2 border-muted", children })
2160
2713
  ] });
2161
2714
  }
2162
2715
 
2163
2716
  // src/widgets/ObjectWidget.tsx
2164
- import { Fragment as Fragment3, jsx as jsx21 } from "react/jsx-runtime";
2717
+ import { Fragment as Fragment3, jsx as jsx24 } from "react/jsx-runtime";
2165
2718
  function buildSubSchema(prop) {
2166
2719
  const properties = prop.properties;
2167
2720
  if (!properties) return null;
@@ -2227,7 +2780,7 @@ function ObjectWidget({
2227
2780
  globalOptions,
2228
2781
  layout
2229
2782
  );
2230
- const content = /* @__PURE__ */ jsx21(
2783
+ const content = /* @__PURE__ */ jsx24(
2231
2784
  Renderer,
2232
2785
  {
2233
2786
  schema: subSchema,
@@ -2242,8 +2795,8 @@ function ObjectWidget({
2242
2795
  onBlur: onBlur ? () => onBlur(id, value) : void 0
2243
2796
  }
2244
2797
  );
2245
- if (displayContents) return /* @__PURE__ */ jsx21(Fragment3, { children: content });
2246
- return /* @__PURE__ */ jsx21(
2798
+ if (displayContents) return /* @__PURE__ */ jsx24(Fragment3, { children: content });
2799
+ return /* @__PURE__ */ jsx24(
2247
2800
  ContainerTemplate,
2248
2801
  {
2249
2802
  label,
@@ -2257,15 +2810,15 @@ function ObjectWidget({
2257
2810
 
2258
2811
  // src/widgets/ArrayWidget.tsx
2259
2812
  import { useCallback as useCallback5, useMemo as useMemo5 } from "react";
2260
- import { Plus as Plus2 } from "lucide-react";
2261
- import { Button as Button5 } from "@petrarca/sonnet-ui";
2813
+ import { Plus as Plus3 } from "lucide-react";
2814
+ import { Button as Button6 } from "@petrarca/sonnet-ui";
2262
2815
  import { applySchemaDefaults } from "@petrarca/sonnet-core/schema";
2263
2816
  import { FORM_ITEM_ID_FIELD, generateId } from "@petrarca/sonnet-core";
2264
2817
 
2265
2818
  // src/templates/DefaultArrayItemTemplate.tsx
2266
2819
  import { Trash2, ChevronUp, ChevronDown as ChevronDown3 } from "lucide-react";
2267
- import { Button as Button4 } from "@petrarca/sonnet-ui";
2268
- import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
2820
+ import { Button as Button5 } from "@petrarca/sonnet-ui";
2821
+ import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
2269
2822
  function DefaultArrayItemTemplate({
2270
2823
  children,
2271
2824
  index,
@@ -2279,16 +2832,16 @@ function DefaultArrayItemTemplate({
2279
2832
  onMoveDown
2280
2833
  }) {
2281
2834
  const showControls = canRemove || orderable;
2282
- return /* @__PURE__ */ jsxs14(
2835
+ return /* @__PURE__ */ jsxs16(
2283
2836
  "div",
2284
2837
  {
2285
2838
  "data-fid": fid,
2286
2839
  className: "relative flex gap-2 rounded-md border border-border/50 bg-muted/20 p-3",
2287
2840
  children: [
2288
- /* @__PURE__ */ jsx22("div", { className: "flex-1 min-w-0", children }),
2289
- showControls && /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1 pt-0.5", children: [
2290
- orderable && index > 0 && /* @__PURE__ */ jsx22(
2291
- Button4,
2841
+ /* @__PURE__ */ jsx25("div", { className: "flex-1 min-w-0", children }),
2842
+ showControls && /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-1 pt-0.5", children: [
2843
+ orderable && index > 0 && /* @__PURE__ */ jsx25(
2844
+ Button5,
2292
2845
  {
2293
2846
  type: "button",
2294
2847
  variant: "ghost",
@@ -2296,11 +2849,11 @@ function DefaultArrayItemTemplate({
2296
2849
  disabled,
2297
2850
  onClick: onMoveUp,
2298
2851
  title: "Move up",
2299
- children: /* @__PURE__ */ jsx22(ChevronUp, { className: "h-3.5 w-3.5" })
2852
+ children: /* @__PURE__ */ jsx25(ChevronUp, { className: "h-3.5 w-3.5" })
2300
2853
  }
2301
2854
  ),
2302
- orderable && index < total - 1 && /* @__PURE__ */ jsx22(
2303
- Button4,
2855
+ orderable && index < total - 1 && /* @__PURE__ */ jsx25(
2856
+ Button5,
2304
2857
  {
2305
2858
  type: "button",
2306
2859
  variant: "ghost",
@@ -2308,11 +2861,11 @@ function DefaultArrayItemTemplate({
2308
2861
  disabled,
2309
2862
  onClick: onMoveDown,
2310
2863
  title: "Move down",
2311
- children: /* @__PURE__ */ jsx22(ChevronDown3, { className: "h-3.5 w-3.5" })
2864
+ children: /* @__PURE__ */ jsx25(ChevronDown3, { className: "h-3.5 w-3.5" })
2312
2865
  }
2313
2866
  ),
2314
- canRemove && /* @__PURE__ */ jsx22(
2315
- Button4,
2867
+ canRemove && /* @__PURE__ */ jsx25(
2868
+ Button5,
2316
2869
  {
2317
2870
  type: "button",
2318
2871
  variant: "ghost",
@@ -2321,7 +2874,7 @@ function DefaultArrayItemTemplate({
2321
2874
  onClick: onRemove,
2322
2875
  title: "Remove item",
2323
2876
  className: "text-muted-foreground hover:text-destructive",
2324
- children: /* @__PURE__ */ jsx22(Trash2, { className: "h-3.5 w-3.5" })
2877
+ children: /* @__PURE__ */ jsx25(Trash2, { className: "h-3.5 w-3.5" })
2325
2878
  }
2326
2879
  )
2327
2880
  ] })
@@ -2338,7 +2891,7 @@ function buildGridTemplate(layout, canRemove, fieldCount, orderable = false) {
2338
2891
  }
2339
2892
 
2340
2893
  // src/templates/DefaultArrayHeaderTemplate.tsx
2341
- import { jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
2894
+ import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
2342
2895
  function DefaultArrayHeaderTemplate({
2343
2896
  itemSchema,
2344
2897
  layout,
@@ -2352,7 +2905,7 @@ function DefaultArrayHeaderTemplate({
2352
2905
  const properties = itemSchema.properties ?? {};
2353
2906
  const order = itemSchema["x-ui-order"];
2354
2907
  const fieldNames = order ? order.filter((k) => k in properties) : Object.keys(properties);
2355
- return /* @__PURE__ */ jsxs15(
2908
+ return /* @__PURE__ */ jsxs17(
2356
2909
  "div",
2357
2910
  {
2358
2911
  style: {
@@ -2361,16 +2914,16 @@ function DefaultArrayHeaderTemplate({
2361
2914
  gap: `${gap}px`
2362
2915
  },
2363
2916
  children: [
2364
- fieldNames.map((name) => /* @__PURE__ */ jsx23("span", { className: "text-xs font-medium text-muted-foreground", children: properties[name]?.title ?? name }, name)),
2365
- orderable && /* @__PURE__ */ jsx23("span", {}),
2366
- canRemove && /* @__PURE__ */ jsx23("span", {})
2917
+ fieldNames.map((name) => /* @__PURE__ */ jsx26("span", { className: "text-xs font-medium text-muted-foreground", children: properties[name]?.title ?? name }, name)),
2918
+ orderable && /* @__PURE__ */ jsx26("span", {}),
2919
+ canRemove && /* @__PURE__ */ jsx26("span", {})
2367
2920
  ]
2368
2921
  }
2369
2922
  );
2370
2923
  }
2371
2924
 
2372
2925
  // src/widgets/ArrayWidget.tsx
2373
- import { jsx as jsx24, jsxs as jsxs16 } from "react/jsx-runtime";
2926
+ import { jsx as jsx27, jsxs as jsxs18 } from "react/jsx-runtime";
2374
2927
  function countFields(itemSchema) {
2375
2928
  return Object.keys(itemSchema.properties ?? {}).length;
2376
2929
  }
@@ -2409,9 +2962,9 @@ function buildSubGlobalOptions(layout, isHorizontal) {
2409
2962
  }
2410
2963
  function ArrayHeader({ label, description }) {
2411
2964
  if (!label) return null;
2412
- return /* @__PURE__ */ jsxs16("div", { children: [
2413
- /* @__PURE__ */ jsx24("h4", { className: "text-sm font-medium", children: label }),
2414
- description && /* @__PURE__ */ jsx24("p", { className: "text-xs text-muted-foreground", children: description })
2965
+ return /* @__PURE__ */ jsxs18("div", { children: [
2966
+ /* @__PURE__ */ jsx27("h4", { className: "text-sm font-medium", children: label }),
2967
+ description && /* @__PURE__ */ jsx27("p", { className: "text-xs text-muted-foreground", children: description })
2415
2968
  ] });
2416
2969
  }
2417
2970
  function useArrayHandlers(items, onChange, propertySchema) {
@@ -2459,19 +3012,20 @@ function ArrayWidget({
2459
3012
  description,
2460
3013
  options,
2461
3014
  schema,
2462
- name
3015
+ name,
3016
+ onBlur
2463
3017
  }) {
2464
3018
  const { templates } = useNestedFormContext();
2465
3019
  const items = useMemo5(() => ensureItemIds(value), [value]);
2466
3020
  const { handleItemChange, handleAdd, handleRemove, handleMove } = useArrayHandlers(items, onChange, propertySchema);
2467
3021
  const itemSchema = propertySchema.items;
2468
3022
  if (!itemSchema?.properties) {
2469
- return /* @__PURE__ */ jsx24("p", { className: "text-sm text-destructive", children: 'ArrayWidget requires items with type "object" and properties.' });
3023
+ return /* @__PURE__ */ jsx27("p", { className: "text-sm text-destructive", children: 'ArrayWidget requires items with type "object" and properties.' });
2470
3024
  }
2471
3025
  const layout = resolveLayout2(uiSchema, propertySchema);
2472
3026
  const isHorizontal = layout.direction === "horizontal";
2473
3027
  const subGlobalOptions = buildSubGlobalOptions(layout, isHorizontal);
2474
- return /* @__PURE__ */ jsx24(
3028
+ return /* @__PURE__ */ jsx27(
2475
3029
  ArrayWidgetInner,
2476
3030
  {
2477
3031
  items,
@@ -2493,7 +3047,8 @@ function ArrayWidget({
2493
3047
  onItemChange: handleItemChange,
2494
3048
  onAdd: handleAdd,
2495
3049
  onRemove: handleRemove,
2496
- onMove: handleMove
3050
+ onMove: handleMove,
3051
+ onBlur
2497
3052
  }
2498
3053
  );
2499
3054
  }
@@ -2553,7 +3108,8 @@ function ArrayWidgetInner({
2553
3108
  onItemChange,
2554
3109
  onAdd,
2555
3110
  onRemove,
2556
- onMove
3111
+ onMove,
3112
+ onBlur
2557
3113
  }) {
2558
3114
  const constraints = resolveConstraints(
2559
3115
  propertySchema,
@@ -2570,10 +3126,10 @@ function ArrayWidgetInner({
2570
3126
  );
2571
3127
  const fieldCount = countFields(itemSchema);
2572
3128
  const itemOptions = buildItemOptions(isHorizontal, subGlobalOptions);
2573
- return /* @__PURE__ */ jsxs16("div", { className: isHorizontal ? "space-y-1" : "space-y-3", children: [
2574
- /* @__PURE__ */ jsx24(ArrayHeader, { label, description }),
2575
- items.length === 0 && display.emptyMessage && /* @__PURE__ */ jsx24("p", { className: "text-sm text-muted-foreground italic", children: display.emptyMessage }),
2576
- items.length > 0 && /* @__PURE__ */ jsx24(
3129
+ return /* @__PURE__ */ jsxs18("div", { className: isHorizontal ? "space-y-1" : "space-y-3", children: [
3130
+ /* @__PURE__ */ jsx27(ArrayHeader, { label, description }),
3131
+ items.length === 0 && display.emptyMessage && /* @__PURE__ */ jsx27("p", { className: "text-sm text-muted-foreground italic", children: display.emptyMessage }),
3132
+ items.length > 0 && /* @__PURE__ */ jsx27(
2577
3133
  HeaderTemplate,
2578
3134
  {
2579
3135
  itemSchema,
@@ -2583,7 +3139,7 @@ function ArrayWidgetInner({
2583
3139
  fieldCount
2584
3140
  }
2585
3141
  ),
2586
- items.map((item, index) => /* @__PURE__ */ jsx24(
3142
+ items.map((item, index) => /* @__PURE__ */ jsx27(
2587
3143
  ActiveItemTemplate,
2588
3144
  {
2589
3145
  index,
@@ -2597,7 +3153,7 @@ function ArrayWidgetInner({
2597
3153
  onRemove: () => onRemove(index),
2598
3154
  onMoveUp: index > 0 ? () => onMove(index, index - 1) : void 0,
2599
3155
  onMoveDown: index < items.length - 1 ? () => onMove(index, index + 1) : void 0,
2600
- children: /* @__PURE__ */ jsx24(
3156
+ children: /* @__PURE__ */ jsx27(
2601
3157
  ObjectWidget,
2602
3158
  {
2603
3159
  id: `field-${name}-${index}`,
@@ -2612,14 +3168,15 @@ function ArrayWidgetInner({
2612
3168
  description: void 0,
2613
3169
  disabled,
2614
3170
  readonly,
2615
- options: itemOptions
3171
+ options: itemOptions,
3172
+ onBlur
2616
3173
  }
2617
3174
  )
2618
3175
  },
2619
3176
  item[FORM_ITEM_ID_FIELD]
2620
3177
  )),
2621
- canAdd && /* @__PURE__ */ jsxs16(
2622
- Button5,
3178
+ canAdd && /* @__PURE__ */ jsxs18(
3179
+ Button6,
2623
3180
  {
2624
3181
  type: "button",
2625
3182
  variant: "outline",
@@ -2628,7 +3185,7 @@ function ArrayWidgetInner({
2628
3185
  disabled,
2629
3186
  className: "gap-1.5",
2630
3187
  children: [
2631
- /* @__PURE__ */ jsx24(Plus2, { className: "h-3.5 w-3.5" }),
3188
+ /* @__PURE__ */ jsx27(Plus3, { className: "h-3.5 w-3.5" }),
2632
3189
  display.addLabel
2633
3190
  ]
2634
3191
  }
@@ -2637,13 +3194,25 @@ function ArrayWidgetInner({
2637
3194
  }
2638
3195
 
2639
3196
  // src/widgets/QuantityWidget.tsx
2640
- import { useCallback as useCallback6, useRef as useRef4 } from "react";
2641
- import { jsx as jsx25 } from "react/jsx-runtime";
3197
+ import { useCallback as useCallback6, useRef as useRef4, useState as useState8 } from "react";
3198
+ import { unitRangesOf } from "@petrarca/sonnet-core/schema";
3199
+ import { jsx as jsx28 } from "react/jsx-runtime";
2642
3200
  function isUnitOption(u) {
2643
3201
  return typeof u === "object" && u !== null && typeof u.value === "string" && typeof u.label === "string";
2644
3202
  }
2645
- function parseQuantityOptions(options) {
2646
- const units = Array.isArray(options.units) ? options.units.filter(isUnitOption) : [];
3203
+ function schemaUnits(propertySchema) {
3204
+ const codes = propertySchema.properties?.unit?.enum;
3205
+ return Array.isArray(codes) ? codes.map((c) => ({ value: String(c), label: String(c) })) : [];
3206
+ }
3207
+ function withSchemaRanges(units, propertySchema) {
3208
+ const ranges = unitRangesOf(propertySchema) ?? {};
3209
+ return units.map((u) => {
3210
+ const range = ranges[u.value];
3211
+ return range ? { ...u, min: range.min, max: range.max } : u;
3212
+ });
3213
+ }
3214
+ function parseQuantityOptions(options, fallbackUnits) {
3215
+ const units = Array.isArray(options.units) ? options.units.filter(isUnitOption) : fallbackUnits;
2647
3216
  const precision = typeof options.precision === "number" ? options.precision : void 0;
2648
3217
  const step = typeof options.step === "number" ? options.step : void 0;
2649
3218
  const unitPosition = options.unitPosition === "prefix" || options.unitPosition === "suffix" ? options.unitPosition : void 0;
@@ -2668,47 +3237,1169 @@ function QuantityWidget({
2668
3237
  }) {
2669
3238
  const error = firstError(rawErrors);
2670
3239
  const {
2671
- units,
3240
+ units: optionUnits,
2672
3241
  precision,
2673
3242
  step,
2674
3243
  unitPosition: unitPositionOpt
2675
- } = parseQuantityOptions(options);
2676
- const quantityValue = value != null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3244
+ } = parseQuantityOptions(options, schemaUnits(propertySchema));
3245
+ const units = withSchemaRanges(optionUnits, propertySchema);
3246
+ const [pendingUnit, setPendingUnit] = useState8();
3247
+ const stored = value != null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3248
+ const quantityValue = stored ?? (pendingUnit ? { value: void 0, unit: pendingUnit } : void 0);
3249
+ const handleChange = useCallback6(
3250
+ (next) => {
3251
+ if (next.value !== void 0) return onChange(next);
3252
+ setPendingUnit(next.unit);
3253
+ onChange(void 0);
3254
+ },
3255
+ [onChange]
3256
+ );
2677
3257
  const valueRef = useRef4(value);
2678
3258
  valueRef.current = value;
2679
3259
  const handleBlur = useCallback6(() => {
2680
3260
  onBlur?.(id, valueRef.current);
2681
3261
  }, [id, onBlur]);
2682
- return /* @__PURE__ */ jsx25(
3262
+ return /* @__PURE__ */ jsx28(
2683
3263
  FormQuantityInput,
2684
3264
  {
2685
- id,
2686
- name,
2687
- value: quantityValue,
2688
- onChange,
2689
- onBlur: handleBlur,
2690
- units,
2691
- unitPosition: unitPositionOpt,
2692
- precision,
2693
- step,
2694
- min: propertySchema.minimum,
2695
- max: propertySchema.maximum,
2696
- label,
2697
- labelPlacement: options?.labelPlacement,
2698
- description,
2699
- error,
2700
- placeholder,
2701
- required,
2702
- disabled,
2703
- readOnly: readonly,
2704
- autoFocus: autofocus
3265
+ id,
3266
+ name,
3267
+ value: quantityValue,
3268
+ onChange: handleChange,
3269
+ onBlur: handleBlur,
3270
+ units,
3271
+ freeUnit: propertySchema.properties?.unit !== void 0,
3272
+ unitPosition: unitPositionOpt,
3273
+ precision,
3274
+ step,
3275
+ min: propertySchema.minimum,
3276
+ max: propertySchema.maximum,
3277
+ label,
3278
+ labelPlacement: options?.labelPlacement,
3279
+ description,
3280
+ error,
3281
+ placeholder,
3282
+ required,
3283
+ disabled,
3284
+ readOnly: readonly,
3285
+ autoFocus: autofocus
3286
+ }
3287
+ );
3288
+ }
3289
+
3290
+ // src/widgets/CodedValueWidget.tsx
3291
+ import * as React13 from "react";
3292
+ import { ChevronsUpDown as ChevronsUpDown4, Loader2, TriangleAlert, X as X5 } from "lucide-react";
3293
+ import { cn as cn12, EMPTY_VALUE } from "@petrarca/sonnet-core";
3294
+ import {
3295
+ bindingOf,
3296
+ codingKey as codingKey5
3297
+ } from "@petrarca/sonnet-core/schema";
3298
+ import {
3299
+ useTerminology
3300
+ } from "@petrarca/sonnet-core/terminology";
3301
+ import {
3302
+ Badge as Badge4,
3303
+ Button as Button7,
3304
+ CodedValueDisplay as CodedValueDisplay2,
3305
+ Popover as Popover3,
3306
+ PopoverContent as PopoverContent3,
3307
+ PopoverTrigger as PopoverTrigger3
3308
+ } from "@petrarca/sonnet-ui";
3309
+
3310
+ // src/widgets/codedValue/codedValueModel.ts
3311
+ import {
3312
+ codingKey
3313
+ } from "@petrarca/sonnet-core/schema";
3314
+ var CODING_KEYS = ["system", "code", "version", "display"];
3315
+ function shapeOf(prop) {
3316
+ if (prop?.type === "string") return "code";
3317
+ return prop?.anyOf?.some((m) => m.type === "string") ? "codingOrText" : "coding";
3318
+ }
3319
+ function fieldModelOf(prop, options) {
3320
+ const multiple = prop.type === "array";
3321
+ const keep = Array.isArray(options?.keep) ? options.keep.filter(
3322
+ (k) => typeof k === "string"
3323
+ ) : [];
3324
+ return {
3325
+ multiple,
3326
+ shape: shapeOf(multiple ? prop.items : prop),
3327
+ store: typeof options?.store === "string" ? options.store : "code",
3328
+ keep: keep.filter((k) => !CODING_KEYS.includes(k))
3329
+ };
3330
+ }
3331
+ function takesText(field, binding) {
3332
+ if (field.shape === "codingOrText") return true;
3333
+ return field.shape === "code" && binding?.strength === "extensible";
3334
+ }
3335
+ function asCoding(value) {
3336
+ if (typeof value !== "object" || value === null) return void 0;
3337
+ const c = value;
3338
+ return typeof c.code === "string" && c.code ? { ...c, system: c.system ?? "", code: c.code } : void 0;
3339
+ }
3340
+ function entryOf(value, field, binding) {
3341
+ if (typeof value !== "string") {
3342
+ const coding = asCoding(value);
3343
+ return coding && { coding };
3344
+ }
3345
+ if (!value) return void 0;
3346
+ if (field.shape === "codingOrText") return { text: value };
3347
+ return {
3348
+ coding: {
3349
+ system: binding?.system ?? "",
3350
+ code: value,
3351
+ ...binding?.version && { version: binding.version }
3352
+ }
3353
+ };
3354
+ }
3355
+ function entriesOf(value, field, binding) {
3356
+ const values = field.multiple ? Array.isArray(value) ? value : [] : [value];
3357
+ return values.map((v) => entryOf(v, field, binding)).filter((e) => e !== void 0);
3358
+ }
3359
+ function entryKey(e) {
3360
+ return e.coding ? codingKey(e.coding) : `text:${e.text ?? ""}`;
3361
+ }
3362
+ function toStored(hit, field, binding) {
3363
+ if (field.shape === "code") return String(hit[field.store] ?? hit.code);
3364
+ const version = hit.version ?? binding?.version;
3365
+ const kept = field.keep.filter((k) => hit[k] != null);
3366
+ return {
3367
+ system: hit.system,
3368
+ code: hit.code,
3369
+ ...version && { version },
3370
+ ...hit.display && { display: hit.display },
3371
+ ...Object.fromEntries(kept.map((k) => [k, hit[k]]))
3372
+ };
3373
+ }
3374
+ function storedKey(v, field, binding) {
3375
+ const entry = entryOf(v, field, binding);
3376
+ return entry ? entryKey(entry) : void 0;
3377
+ }
3378
+ function withAnswer(value, answer, field, binding) {
3379
+ if (!field.multiple) return answer;
3380
+ const current = Array.isArray(value) ? value : [];
3381
+ const key = storedKey(answer, field, binding);
3382
+ const others = current.filter((v) => storedKey(v, field, binding) !== key);
3383
+ if (others.length < current.length) return others.length ? others : void 0;
3384
+ return [...current, answer];
3385
+ }
3386
+ function withoutAnswer(value, key, field, binding) {
3387
+ if (!field.multiple) return void 0;
3388
+ const current = Array.isArray(value) ? value : [];
3389
+ const others = current.filter((v) => storedKey(v, field, binding) !== key);
3390
+ return others.length ? others : void 0;
3391
+ }
3392
+
3393
+ // src/widgets/codedValue/useCodedValueOptions.ts
3394
+ import { useCallback as useCallback7, useEffect as useEffect4, useRef as useRef5, useState as useState9 } from "react";
3395
+ var PAGE_SIZE = 50;
3396
+ var DEBOUNCE_MS = 250;
3397
+ function useDebounced(text) {
3398
+ const [settled, setSettled] = useState9(text);
3399
+ useEffect4(() => {
3400
+ const t = setTimeout(() => setSettled(text), DEBOUNCE_MS);
3401
+ return () => clearTimeout(t);
3402
+ }, [text]);
3403
+ return settled;
3404
+ }
3405
+ function fitsOnePage(result) {
3406
+ return result.total !== void 0 ? result.total <= PAGE_SIZE : result.codings.length < PAGE_SIZE;
3407
+ }
3408
+ function moreAfter(result) {
3409
+ return result.total !== void 0 ? result.codings.length < result.total : false;
3410
+ }
3411
+ function usePagedSearch(service, list) {
3412
+ const [page, setPage] = useState9();
3413
+ const [loading, setLoading] = useState9(false);
3414
+ const [error, setError] = useState9();
3415
+ const request = useRef5(0);
3416
+ const shown = useRef5(null);
3417
+ const run = useCallback7(
3418
+ (text, offset, before) => {
3419
+ if (!service || !list) return Promise.resolve(void 0);
3420
+ const id = ++request.current;
3421
+ setLoading(true);
3422
+ return service.search({ ...list, text, limit: PAGE_SIZE, offset }).then((r) => {
3423
+ if (id !== request.current) return void 0;
3424
+ shown.current = text ?? "";
3425
+ const result = { ...r, codings: [...before, ...r.codings] };
3426
+ setPage({ result, hasMore: moreAfter(result) });
3427
+ setError(void 0);
3428
+ return r;
3429
+ }).catch((e) => {
3430
+ if (id === request.current) setError(String(e));
3431
+ return void 0;
3432
+ }).finally(() => {
3433
+ if (id === request.current) setLoading(false);
3434
+ });
3435
+ },
3436
+ [service, list]
3437
+ );
3438
+ const clear = useCallback7(() => {
3439
+ request.current++;
3440
+ shown.current = null;
3441
+ setPage(void 0);
3442
+ setLoading(false);
3443
+ }, []);
3444
+ const isShown = useCallback7((text) => shown.current === text, []);
3445
+ return { page, loading, error, run, clear, isShown };
3446
+ }
3447
+ function useCodedValueOptions(service, list, forced, minChars = 1) {
3448
+ const [search, setSearch] = useState9("");
3449
+ const [decided, setDecided] = useState9(forced);
3450
+ const text = useDebounced(search.trim());
3451
+ const { page, loading, error, run, clear, isShown } = usePagedSearch(
3452
+ service,
3453
+ list
3454
+ );
3455
+ const mode = forced ?? decided;
3456
+ useEffect4(() => {
3457
+ if (forced) return;
3458
+ void run(void 0, 0, []).then(
3459
+ (r) => r && setDecided(fitsOnePage(r) ? "list" : "search")
3460
+ );
3461
+ }, [forced, run]);
3462
+ useEffect4(() => {
3463
+ if (!mode) return;
3464
+ if (mode === "search" && text.length < Math.max(minChars, 1)) clear();
3465
+ else if (!isShown(text)) void run(text || void 0, 0, []);
3466
+ }, [mode, text, minChars, run, clear, isShown]);
3467
+ const loadMore = useCallback7(() => {
3468
+ const loaded = page?.result.codings ?? [];
3469
+ void run(text || void 0, loaded.length, loaded);
3470
+ }, [page, run, text]);
3471
+ return {
3472
+ hits: page?.result.codings ?? [],
3473
+ total: page?.result.total,
3474
+ hasMore: page?.hasMore ?? false,
3475
+ loading,
3476
+ error,
3477
+ mode,
3478
+ search,
3479
+ setSearch,
3480
+ minChars,
3481
+ loadMore
3482
+ };
3483
+ }
3484
+
3485
+ // src/widgets/codedValue/useStoredLabel.ts
3486
+ import { useEffect as useEffect5, useState as useState10 } from "react";
3487
+ import {
3488
+ codingKey as codingKey2
3489
+ } from "@petrarca/sonnet-core/schema";
3490
+ async function fetchLabel(service, coding, valueSet, version) {
3491
+ if (coding.system) return (await service.lookup(coding))?.display;
3492
+ if (!valueSet) return void 0;
3493
+ const { codings } = await service.search({
3494
+ valueSet,
3495
+ version,
3496
+ text: coding.code
3497
+ });
3498
+ return codings.find((c) => c.code === coding.code)?.display;
3499
+ }
3500
+ function labelQuery(coding, binding) {
3501
+ const system = coding?.system ?? "";
3502
+ const code = coding?.code ?? "";
3503
+ return {
3504
+ own: coding?.display,
3505
+ system,
3506
+ code,
3507
+ version: coding?.version ?? binding?.version,
3508
+ valueSet: binding?.valueSet,
3509
+ key: codingKey2({ system, code })
3510
+ };
3511
+ }
3512
+ function useStoredLabel(service, coding, binding) {
3513
+ const [fetched, setFetched] = useState10();
3514
+ const { own, system, code, version, valueSet, key } = labelQuery(
3515
+ coding,
3516
+ binding
3517
+ );
3518
+ useEffect5(() => {
3519
+ if (!service || !code || own) return;
3520
+ let live = true;
3521
+ fetchLabel(service, { system, code, version }, valueSet, version).then((label) => live && setFetched({ key, label })).catch(() => live && setFetched({ key }));
3522
+ return () => {
3523
+ live = false;
3524
+ };
3525
+ }, [service, system, code, version, valueSet, own, key]);
3526
+ return own ?? (fetched?.key === key ? fetched.label : void 0);
3527
+ }
3528
+
3529
+ // src/widgets/codedValue/useListCheck.ts
3530
+ import { useEffect as useEffect6, useMemo as useMemo6, useState as useState11 } from "react";
3531
+ import {
3532
+ codingKey as codingKey3
3533
+ } from "@petrarca/sonnet-core/schema";
3534
+ var NONE = /* @__PURE__ */ new Map();
3535
+ function checks(binding, storesCodes) {
3536
+ return binding?.strength === "required" && !!(binding.valueSet || binding.system) && storesCodes;
3537
+ }
3538
+ async function warningOf(validate, binding, coding) {
3539
+ try {
3540
+ const result = await validate({
3541
+ valueSet: binding.valueSet,
3542
+ system: binding.system,
3543
+ version: binding.version,
3544
+ code: coding.code
3545
+ });
3546
+ if (result.inList) return void 0;
3547
+ return [
3548
+ codingKey3(coding),
3549
+ result.message ?? `${coding.code} is not in the list.`
3550
+ ];
3551
+ } catch {
3552
+ return void 0;
3553
+ }
3554
+ }
3555
+ function useListCheck(service, binding, codings, storesCodes) {
3556
+ const [warnings, setWarnings] = useState11(NONE);
3557
+ const validate = useMemo6(() => service?.validate?.bind(service), [service]);
3558
+ const active = !!validate && checks(binding, storesCodes);
3559
+ const codes = active ? JSON.stringify(codings.map((c) => [c.system, c.code])) : "[]";
3560
+ const valueSet = binding?.valueSet;
3561
+ const system = binding?.system;
3562
+ const version = binding?.version;
3563
+ useEffect6(() => {
3564
+ const stored = JSON.parse(codes).map(
3565
+ ([s, code]) => ({ system: s, code })
3566
+ );
3567
+ if (!validate || !active || stored.length === 0) {
3568
+ setWarnings(NONE);
3569
+ return;
3570
+ }
3571
+ const list = { valueSet, system, version, strength: "required" };
3572
+ let live = true;
3573
+ void Promise.all(stored.map((c) => warningOf(validate, list, c))).then(
3574
+ (found) => {
3575
+ if (!live) return;
3576
+ const entries = found.filter((w) => w !== void 0);
3577
+ setWarnings(entries.length ? new Map(entries) : NONE);
3578
+ }
3579
+ );
3580
+ return () => {
3581
+ live = false;
3582
+ };
3583
+ }, [active, validate, codes, valueSet, system, version]);
3584
+ return warnings;
3585
+ }
3586
+
3587
+ // src/widgets/codedValue/HitList.tsx
3588
+ import { Check as Check3 } from "lucide-react";
3589
+ import { cn as cn11 } from "@petrarca/sonnet-core";
3590
+ import { codingKey as codingKey4 } from "@petrarca/sonnet-core/schema";
3591
+ import {
3592
+ CodedValueDisplay,
3593
+ Command as Command3,
3594
+ CommandEmpty as CommandEmpty3,
3595
+ CommandGroup as CommandGroup3,
3596
+ CommandInput as CommandInput3,
3597
+ CommandItem as CommandItem3,
3598
+ CommandList as CommandList3
3599
+ } from "@petrarca/sonnet-ui";
3600
+ import { jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
3601
+ function emptyMessage(o) {
3602
+ if (o.loading) return "Loading...";
3603
+ if (o.error) return "The list could not be loaded.";
3604
+ const typed = o.search.trim().length;
3605
+ if (o.mode === "search" && typed < o.minChars)
3606
+ return o.minChars > 1 && typed > 0 ? `Type at least ${o.minChars} characters...` : "Type to search...";
3607
+ return "No matches.";
3608
+ }
3609
+ function HitItem({
3610
+ hit,
3611
+ selected,
3612
+ onPick
3613
+ }) {
3614
+ return /* @__PURE__ */ jsxs19(CommandItem3, { value: codingKey4(hit), onSelect: () => onPick(hit), children: [
3615
+ /* @__PURE__ */ jsx29(
3616
+ Check3,
3617
+ {
3618
+ className: cn11(
3619
+ "mr-2 h-4 w-4 shrink-0",
3620
+ selected ? "opacity-100" : "opacity-0"
3621
+ )
3622
+ }
3623
+ ),
3624
+ /* @__PURE__ */ jsxs19("div", { className: "min-w-0", children: [
3625
+ /* @__PURE__ */ jsx29(CodedValueDisplay, { code: hit.code, display: hit.display }),
3626
+ hit.path && hit.path.length > 0 && /* @__PURE__ */ jsx29("div", { className: "truncate text-xs text-muted-foreground", children: hit.path.join(" \u203A ") })
3627
+ ] })
3628
+ ] });
3629
+ }
3630
+ function HitList({
3631
+ options,
3632
+ selected,
3633
+ freeText,
3634
+ onPick,
3635
+ onFreeText
3636
+ }) {
3637
+ return /* @__PURE__ */ jsxs19(Command3, { shouldFilter: false, children: [
3638
+ /* @__PURE__ */ jsx29(
3639
+ CommandInput3,
3640
+ {
3641
+ placeholder: "Search...",
3642
+ value: options.search,
3643
+ onValueChange: options.setSearch
3644
+ }
3645
+ ),
3646
+ /* @__PURE__ */ jsxs19(CommandList3, { children: [
3647
+ /* @__PURE__ */ jsx29(CommandEmpty3, { children: emptyMessage(options) }),
3648
+ /* @__PURE__ */ jsxs19(CommandGroup3, { children: [
3649
+ options.hits.map((hit) => /* @__PURE__ */ jsx29(
3650
+ HitItem,
3651
+ {
3652
+ hit,
3653
+ selected: selected.has(codingKey4(hit)),
3654
+ onPick
3655
+ },
3656
+ codingKey4(hit)
3657
+ )),
3658
+ options.hasMore && /* @__PURE__ */ jsx29(CommandItem3, { value: "__more", onSelect: options.loadMore, children: /* @__PURE__ */ jsxs19("span", { className: "text-sm text-muted-foreground", children: [
3659
+ "Show more",
3660
+ options.total !== void 0 && ` (${options.hits.length} of ${options.total})`
3661
+ ] }) }),
3662
+ freeText && /* @__PURE__ */ jsx29(CommandItem3, { value: "__text", onSelect: () => onFreeText(freeText), children: /* @__PURE__ */ jsxs19("span", { className: "text-sm", children: [
3663
+ "Use \u201C",
3664
+ freeText,
3665
+ "\u201D"
3666
+ ] }) })
3667
+ ] })
3668
+ ] })
3669
+ ] });
3670
+ }
3671
+
3672
+ // src/widgets/codedValue/InlineIconButton.tsx
3673
+ import { jsx as jsx30 } from "react/jsx-runtime";
3674
+ function InlineIconButton({
3675
+ label,
3676
+ onAct,
3677
+ children
3678
+ }) {
3679
+ const act = (e) => {
3680
+ e.preventDefault();
3681
+ e.stopPropagation();
3682
+ onAct();
3683
+ };
3684
+ return /* @__PURE__ */ jsx30(
3685
+ "span",
3686
+ {
3687
+ role: "button",
3688
+ "aria-label": label,
3689
+ tabIndex: -1,
3690
+ className: "rounded-sm opacity-50 hover:opacity-100",
3691
+ onPointerDown: act,
3692
+ children
3693
+ }
3694
+ );
3695
+ }
3696
+
3697
+ // src/widgets/codedValue/listOptions.ts
3698
+ import * as React12 from "react";
3699
+ function modeOption(options) {
3700
+ const mode = options?.mode;
3701
+ return mode === "list" || mode === "search" ? mode : void 0;
3702
+ }
3703
+ function minCharsOption(options) {
3704
+ const n = options?.minChars;
3705
+ return typeof n === "number" && n >= 1 ? n : 1;
3706
+ }
3707
+ function useListRef(binding) {
3708
+ const { valueSet, system, version } = binding ?? {};
3709
+ return React12.useMemo(
3710
+ () => valueSet || system ? { valueSet, system, version } : void 0,
3711
+ [valueSet, system, version]
3712
+ );
3713
+ }
3714
+
3715
+ // src/widgets/CodedValueWidget.tsx
3716
+ import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
3717
+ function freeTextOf(o, open) {
3718
+ const text = o.search.trim();
3719
+ if (!open || !text) return void 0;
3720
+ return o.hits.some((h) => h.code === text) ? void 0 : text;
3721
+ }
3722
+ function EntryText({
3723
+ entry,
3724
+ shown
3725
+ }) {
3726
+ const key = entryKey(entry);
3727
+ const known = shown.picked.has(key);
3728
+ const looked = useStoredLabel(
3729
+ known || !shown.lookup ? null : shown.service,
3730
+ entry.coding,
3731
+ shown.binding
3732
+ );
3733
+ if (!entry.coding)
3734
+ return /* @__PURE__ */ jsx31("span", { className: "truncate text-sm", children: entry.text });
3735
+ return /* @__PURE__ */ jsx31(
3736
+ CodedValueDisplay2,
3737
+ {
3738
+ code: entry.coding.code,
3739
+ display: known ? shown.picked.get(key) : looked,
3740
+ system: entry.coding.system || void 0
3741
+ }
3742
+ );
3743
+ }
3744
+ function Chip({
3745
+ entry,
3746
+ shown,
3747
+ onRemove
3748
+ }) {
3749
+ return /* @__PURE__ */ jsxs20(Badge4, { variant: "secondary", className: "max-w-full gap-1 font-normal", children: [
3750
+ /* @__PURE__ */ jsx31(EntryText, { entry, shown }),
3751
+ onRemove && /* @__PURE__ */ jsx31(
3752
+ InlineIconButton,
3753
+ {
3754
+ label: "Remove",
3755
+ onAct: () => onRemove(entryKey(entry)),
3756
+ children: /* @__PURE__ */ jsx31(X5, { className: "h-3 w-3" })
3757
+ }
3758
+ )
3759
+ ] });
3760
+ }
3761
+ function Entries({
3762
+ entries,
3763
+ field,
3764
+ shown,
3765
+ onRemove
3766
+ }) {
3767
+ if (!field.multiple) return /* @__PURE__ */ jsx31(EntryText, { entry: entries[0], shown });
3768
+ return /* @__PURE__ */ jsx31("span", { className: "flex min-w-0 flex-wrap gap-1", children: entries.map((e) => /* @__PURE__ */ jsx31(Chip, { entry: e, shown, onRemove }, entryKey(e))) });
3769
+ }
3770
+ function TriggerIcons3({
3771
+ loading,
3772
+ onClear
3773
+ }) {
3774
+ return /* @__PURE__ */ jsxs20("span", { className: "ml-2 flex shrink-0 items-center gap-1", children: [
3775
+ loading && /* @__PURE__ */ jsx31(Loader2, { className: "h-4 w-4 animate-spin opacity-50" }),
3776
+ onClear && /* @__PURE__ */ jsx31(InlineIconButton, { label: "Clear selection", onAct: onClear, children: /* @__PURE__ */ jsx31(X5, { className: "h-4 w-4" }) }),
3777
+ /* @__PURE__ */ jsx31(ChevronsUpDown4, { className: "h-4 w-4 opacity-50" })
3778
+ ] });
3779
+ }
3780
+ function fieldStateOf(props) {
3781
+ const binding = bindingOf(props.propertySchema);
3782
+ const field = fieldModelOf(props.propertySchema, props.options);
3783
+ return {
3784
+ binding,
3785
+ field,
3786
+ entries: entriesOf(props.value, field, binding),
3787
+ storesCodes: field.store === "code"
3788
+ };
3789
+ }
3790
+ function useAnswers(props, state) {
3791
+ const { value, onChange } = props;
3792
+ const { binding, field } = state;
3793
+ const [picked, setPicked] = React13.useState(
3794
+ () => /* @__PURE__ */ new Map()
3795
+ );
3796
+ const pick = (hit) => {
3797
+ setPicked((m) => new Map(m).set(codingKey5(hit), hit.display));
3798
+ onChange(withAnswer(value, toStored(hit, field, binding), field, binding));
3799
+ };
3800
+ const takeText = (typed) => onChange(withAnswer(value, typed, field, binding));
3801
+ const remove = (key) => onChange(withoutAnswer(value, key, field, binding));
3802
+ return { picked, pick, takeText, remove };
3803
+ }
3804
+ function EditableCodedValue({
3805
+ props,
3806
+ state,
3807
+ service
3808
+ }) {
3809
+ const { id, value, onChange, onBlur, required, rawErrors, placeholder } = props;
3810
+ const { binding, field, entries } = state;
3811
+ const [open, setOpen] = React13.useState(false);
3812
+ const list = useListRef(binding);
3813
+ const options = useCodedValueOptions(
3814
+ service,
3815
+ list,
3816
+ modeOption(props.options),
3817
+ minCharsOption(props.options)
3818
+ );
3819
+ const answers = useAnswers(props, state);
3820
+ const shown = {
3821
+ binding,
3822
+ service,
3823
+ picked: answers.picked,
3824
+ lookup: state.storesCodes
3825
+ };
3826
+ const empty = entries.length === 0;
3827
+ const close = (next) => {
3828
+ setOpen(next);
3829
+ if (!next) {
3830
+ options.setSearch("");
3831
+ onBlur?.(id, value);
3832
+ }
3833
+ };
3834
+ const done = () => {
3835
+ if (!field.multiple) close(false);
3836
+ };
3837
+ const clear = () => onChange(void 0);
3838
+ return /* @__PURE__ */ jsxs20(Popover3, { open, onOpenChange: close, children: [
3839
+ /* @__PURE__ */ jsx31(PopoverTrigger3, { asChild: true, children: /* @__PURE__ */ jsxs20(
3840
+ Button7,
3841
+ {
3842
+ id,
3843
+ variant: "outline",
3844
+ role: "combobox",
3845
+ "aria-expanded": open,
3846
+ className: cn12(
3847
+ "h-auto min-h-9 w-full justify-between font-normal",
3848
+ empty && "text-muted-foreground",
3849
+ firstError(rawErrors) && "border-destructive"
3850
+ ),
3851
+ children: [
3852
+ empty ? /* @__PURE__ */ jsx31("span", { className: "truncate", children: placeholder || "Select..." }) : /* @__PURE__ */ jsx31(
3853
+ Entries,
3854
+ {
3855
+ entries,
3856
+ field,
3857
+ shown,
3858
+ onRemove: answers.remove
3859
+ }
3860
+ ),
3861
+ /* @__PURE__ */ jsx31(
3862
+ TriggerIcons3,
3863
+ {
3864
+ loading: options.loading,
3865
+ onClear: !empty && !required ? clear : void 0
3866
+ }
3867
+ )
3868
+ ]
3869
+ }
3870
+ ) }),
3871
+ /* @__PURE__ */ jsx31(
3872
+ PopoverContent3,
3873
+ {
3874
+ className: "w-[--radix-popover-trigger-width] p-0",
3875
+ align: "start",
3876
+ children: /* @__PURE__ */ jsx31(
3877
+ HitList,
3878
+ {
3879
+ options,
3880
+ selected: new Set(entries.map(entryKey)),
3881
+ freeText: freeTextOf(options, takesText(field, binding)),
3882
+ onPick: (hit) => {
3883
+ answers.pick(hit);
3884
+ done();
3885
+ },
3886
+ onFreeText: (typed) => {
3887
+ answers.takeText(typed);
3888
+ done();
3889
+ }
3890
+ }
3891
+ )
3892
+ }
3893
+ )
3894
+ ] });
3895
+ }
3896
+ function ReadonlyCodedValue({
3897
+ state,
3898
+ service,
3899
+ note
3900
+ }) {
3901
+ const shown = {
3902
+ binding: state.binding,
3903
+ service,
3904
+ picked: /* @__PURE__ */ new Map(),
3905
+ lookup: state.storesCodes
3906
+ };
3907
+ return /* @__PURE__ */ jsxs20("div", { className: "space-y-1", children: [
3908
+ state.entries.length ? /* @__PURE__ */ jsx31(Entries, { entries: state.entries, field: state.field, shown }) : /* @__PURE__ */ jsx31("span", { className: "text-sm text-muted-foreground", children: EMPTY_VALUE }),
3909
+ note && /* @__PURE__ */ jsx31("p", { className: "text-xs text-muted-foreground", children: note })
3910
+ ] });
3911
+ }
3912
+ function readonlyNote(state, service) {
3913
+ if (!state.binding) return "No list is bound to this field.";
3914
+ if (!service) return "No terminology service is available.";
3915
+ return void 0;
3916
+ }
3917
+ function ListWarningsText({
3918
+ warnings
3919
+ }) {
3920
+ if (warnings.size === 0) return null;
3921
+ return /* @__PURE__ */ jsx31("div", { className: "space-y-0.5", children: [...warnings].map(([key, message]) => /* @__PURE__ */ jsxs20(
3922
+ "p",
3923
+ {
3924
+ className: "flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400",
3925
+ children: [
3926
+ /* @__PURE__ */ jsx31(TriangleAlert, { className: "h-3 w-3 shrink-0" }),
3927
+ message
3928
+ ]
3929
+ },
3930
+ key
3931
+ )) });
3932
+ }
3933
+ function CodedValueBody({
3934
+ props,
3935
+ state,
3936
+ service
3937
+ }) {
3938
+ const note = readonlyNote(state, service);
3939
+ if (props.disabled || props.readonly || note || !service)
3940
+ return /* @__PURE__ */ jsx31(
3941
+ ReadonlyCodedValue,
3942
+ {
3943
+ state,
3944
+ service,
3945
+ note: props.disabled || props.readonly ? void 0 : note
3946
+ }
3947
+ );
3948
+ return /* @__PURE__ */ jsx31(EditableCodedValue, { props, state, service });
3949
+ }
3950
+ function CodedValueField({
3951
+ props
3952
+ }) {
3953
+ const service = useTerminology();
3954
+ const state = fieldStateOf(props);
3955
+ const warnings = useListCheck(
3956
+ service,
3957
+ state.binding,
3958
+ state.entries.flatMap((e) => e.coding ? [e.coding] : []),
3959
+ state.storesCodes
3960
+ );
3961
+ return /* @__PURE__ */ jsxs20("div", { className: "space-y-1", children: [
3962
+ /* @__PURE__ */ jsx31(CodedValueBody, { props, state, service }),
3963
+ /* @__PURE__ */ jsx31(ListWarningsText, { warnings })
3964
+ ] });
3965
+ }
3966
+ function CodedValueWidget(props) {
3967
+ return /* @__PURE__ */ jsx31(
3968
+ FormFieldWrapper,
3969
+ {
3970
+ inputId: props.id,
3971
+ label: props.label,
3972
+ description: props.description,
3973
+ required: props.required,
3974
+ error: firstError(props.rawErrors),
3975
+ children: /* @__PURE__ */ jsx31(CodedValueField, { props })
3976
+ }
3977
+ );
3978
+ }
3979
+
3980
+ // src/widgets/CodeableConceptWidget.tsx
3981
+ import * as React14 from "react";
3982
+ import { Plus as Plus4, X as X6 } from "lucide-react";
3983
+ import { cn as cn13 } from "@petrarca/sonnet-core";
3984
+ import {
3985
+ bindingLists,
3986
+ bindingOf as bindingOf2,
3987
+ CODING_TYPE,
3988
+ codingKey as codingKey6,
3989
+ codingsByList as codingsByList2,
3990
+ conceptOf as conceptOf2,
3991
+ listTitle
3992
+ } from "@petrarca/sonnet-core/schema";
3993
+ import {
3994
+ useTerminology as useTerminology2
3995
+ } from "@petrarca/sonnet-core/terminology";
3996
+ import {
3997
+ Badge as Badge5,
3998
+ Button as Button8,
3999
+ DropdownMenu,
4000
+ DropdownMenuContent,
4001
+ DropdownMenuItem,
4002
+ DropdownMenuTrigger,
4003
+ Popover as Popover4,
4004
+ PopoverAnchor,
4005
+ PopoverContent as PopoverContent4
4006
+ } from "@petrarca/sonnet-ui";
4007
+
4008
+ // src/widgets/codeableConcept/conceptModel.ts
4009
+ import {
4010
+ codingsByList,
4011
+ conceptOf
4012
+ } from "@petrarca/sonnet-core/schema";
4013
+ function valueOf(codings, text) {
4014
+ const t = text?.trim() ? text : void 0;
4015
+ if (codings.length === 0 && !t) return void 0;
4016
+ return {
4017
+ ...codings.length ? { codings } : {},
4018
+ ...t ? { text: t } : {}
4019
+ };
4020
+ }
4021
+ function withRow(value, lists, row, coding) {
4022
+ return withCoding(
4023
+ value,
4024
+ lists,
4025
+ row,
4026
+ coding && { ...coding, userSelected: true }
4027
+ );
4028
+ }
4029
+ function withSuggested(value, lists, row, suggestion) {
4030
+ if (codingsByList(value, lists).byList[row]) return conceptOf(value);
4031
+ const { coding, match } = suggestion;
4032
+ return withCoding(value, lists, row, {
4033
+ ...coding,
4034
+ ...match ? { match } : {}
4035
+ });
4036
+ }
4037
+ function withCoding(value, lists, row, coding) {
4038
+ const { byList: rows, others } = codingsByList(value, lists);
4039
+ const purpose = lists[row]?.purpose;
4040
+ const next = rows.map(
4041
+ (c, i) => i !== row ? c : coding && { ...coding, ...purpose ? { purpose } : {} }
4042
+ );
4043
+ return valueOf(
4044
+ [...next.filter((c) => !!c), ...others],
4045
+ conceptOf(value).text
4046
+ );
4047
+ }
4048
+ function withText(value, text) {
4049
+ return valueOf(conceptOf(value).codings ?? [], text);
4050
+ }
4051
+
4052
+ // src/widgets/CodeableConceptWidget.tsx
4053
+ import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
4054
+ function listSchema(list) {
4055
+ const { valueSet, system, version } = list;
4056
+ return {
4057
+ ...CODING_TYPE.definition,
4058
+ "x-binding": {
4059
+ ...valueSet ? { valueSet } : {},
4060
+ ...system ? { system } : {},
4061
+ ...version ? { version } : {},
4062
+ strength: "required"
4063
+ }
4064
+ };
4065
+ }
4066
+ var AS_CODING = {
4067
+ multiple: false,
4068
+ shape: "coding",
4069
+ store: "code",
4070
+ keep: []
4071
+ };
4072
+ function offersText(props, extensible) {
4073
+ const text = props.options?.text;
4074
+ return typeof text === "boolean" ? text : extensible;
4075
+ }
4076
+ function CodingChip({
4077
+ label,
4078
+ coding,
4079
+ onOpen,
4080
+ onRemove
4081
+ }) {
4082
+ const mapped = coding.match !== void 0 && !coding.userSelected;
4083
+ return /* @__PURE__ */ jsxs21(
4084
+ Badge5,
4085
+ {
4086
+ variant: "secondary",
4087
+ className: cn13("gap-1 font-normal", onOpen && "cursor-pointer"),
4088
+ title: coding.display ?? coding.code,
4089
+ onClick: onOpen,
4090
+ children: [
4091
+ /* @__PURE__ */ jsx32("span", { className: "text-muted-foreground", children: label }),
4092
+ /* @__PURE__ */ jsx32("span", { className: "font-mono text-xs", children: coding.code }),
4093
+ mapped && /* @__PURE__ */ jsx32("span", { className: "text-muted-foreground", children: "\xB7 mapped" }),
4094
+ onRemove && /* @__PURE__ */ jsx32(InlineIconButton, { label: `Remove ${label}`, onAct: onRemove, children: /* @__PURE__ */ jsx32(X6, { className: "h-3 w-3" }) })
4095
+ ]
4096
+ }
4097
+ );
4098
+ }
4099
+ function MissingChip({
4100
+ label,
4101
+ onOpen
4102
+ }) {
4103
+ return /* @__PURE__ */ jsxs21(
4104
+ Badge5,
4105
+ {
4106
+ variant: "outline",
4107
+ className: cn13(
4108
+ "gap-1 border-dashed font-normal text-muted-foreground",
4109
+ onOpen && "cursor-pointer"
4110
+ ),
4111
+ onClick: onOpen,
4112
+ children: [
4113
+ label,
4114
+ /* @__PURE__ */ jsx32("span", { className: "text-destructive", children: "*" })
4115
+ ]
4116
+ }
4117
+ );
4118
+ }
4119
+ function ListSearch({
4120
+ list,
4121
+ props,
4122
+ selected,
4123
+ onPick
4124
+ }) {
4125
+ const service = useTerminology2();
4126
+ const ref = useListRef({ ...list, strength: "required" });
4127
+ const options = useCodedValueOptions(
4128
+ service,
4129
+ ref,
4130
+ modeOption(props.options),
4131
+ minCharsOption(props.options)
4132
+ );
4133
+ return /* @__PURE__ */ jsxs21("div", { className: "space-y-1", children: [
4134
+ /* @__PURE__ */ jsx32("p", { className: "px-2 pt-2 text-xs font-medium text-muted-foreground", children: listTitle(list) }),
4135
+ /* @__PURE__ */ jsx32(
4136
+ HitList,
4137
+ {
4138
+ options,
4139
+ selected: new Set(selected ? [codingKey6(selected)] : []),
4140
+ onPick,
4141
+ onFreeText: () => void 0
4142
+ }
4143
+ )
4144
+ ] });
4145
+ }
4146
+ function AddCoding({
4147
+ lists,
4148
+ onOpen
4149
+ }) {
4150
+ const chosen = React14.useRef(null);
4151
+ if (lists.length === 0) return null;
4152
+ return /* @__PURE__ */ jsxs21(DropdownMenu, { children: [
4153
+ /* @__PURE__ */ jsx32(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs21(
4154
+ Button8,
4155
+ {
4156
+ type: "button",
4157
+ variant: "ghost",
4158
+ size: "sm",
4159
+ className: "h-6 gap-1 px-2 text-xs",
4160
+ children: [
4161
+ /* @__PURE__ */ jsx32(Plus4, { className: "h-3 w-3" }),
4162
+ "Coding"
4163
+ ]
4164
+ }
4165
+ ) }),
4166
+ /* @__PURE__ */ jsx32(
4167
+ DropdownMenuContent,
4168
+ {
4169
+ align: "start",
4170
+ onCloseAutoFocus: (e) => {
4171
+ if (chosen.current === null) return;
4172
+ e.preventDefault();
4173
+ onOpen(chosen.current);
4174
+ chosen.current = null;
4175
+ },
4176
+ children: lists.map(([list, row]) => /* @__PURE__ */ jsx32(DropdownMenuItem, { onSelect: () => chosen.current = row, children: listTitle(list) }, row))
4177
+ }
4178
+ )
4179
+ ] });
4180
+ }
4181
+ function ListChip({
4182
+ list,
4183
+ row,
4184
+ coding,
4185
+ open,
4186
+ set
4187
+ }) {
4188
+ const onOpen = open && (() => open(row));
4189
+ if (coding)
4190
+ return /* @__PURE__ */ jsx32(
4191
+ CodingChip,
4192
+ {
4193
+ label: listTitle(list),
4194
+ coding,
4195
+ onOpen,
4196
+ onRemove: set && (() => set(row, void 0))
4197
+ }
4198
+ );
4199
+ return list.required ? /* @__PURE__ */ jsx32(MissingChip, { label: listTitle(list), onOpen }) : null;
4200
+ }
4201
+ function ChipLine({ lists, rows, others, open, set, addText }) {
4202
+ const addable = lists.map((l, i) => [l, i]).filter(([l, i]) => i > 0 && !rows[i] && !l.required);
4203
+ return /* @__PURE__ */ jsxs21("div", { className: "flex flex-wrap items-center gap-1", children: [
4204
+ lists.slice(1).map((list, k) => /* @__PURE__ */ jsx32(
4205
+ ListChip,
4206
+ {
4207
+ list,
4208
+ row: k + 1,
4209
+ coding: rows[k + 1],
4210
+ open,
4211
+ set
4212
+ },
4213
+ k + 1
4214
+ )),
4215
+ others.map((c) => /* @__PURE__ */ jsx32(CodingChip, { label: listTitle(c), coding: c }, codingKey6(c))),
4216
+ open && /* @__PURE__ */ jsx32(AddCoding, { lists: addable, onOpen: open }),
4217
+ addText && /* @__PURE__ */ jsxs21(
4218
+ Button8,
4219
+ {
4220
+ type: "button",
4221
+ variant: "ghost",
4222
+ size: "sm",
4223
+ className: "h-6 gap-1 px-2 text-xs",
4224
+ onClick: addText,
4225
+ children: [
4226
+ /* @__PURE__ */ jsx32(Plus4, { className: "h-3 w-3" }),
4227
+ "Text"
4228
+ ]
4229
+ }
4230
+ )
4231
+ ] });
4232
+ }
4233
+ function useConcept(props) {
4234
+ const binding = bindingOf2(props.propertySchema);
4235
+ const lists = binding ? bindingLists(binding) : [];
4236
+ const text = conceptOf2(props.value).text ?? "";
4237
+ const [editing, setEditing] = React14.useState(null);
4238
+ const [showText, setShowText] = React14.useState(false);
4239
+ const offered = offersText(props, binding?.strength === "extensible");
4240
+ const { byList: rows, others } = codingsByList2(props.value, lists);
4241
+ return {
4242
+ lists,
4243
+ rows,
4244
+ others,
4245
+ text,
4246
+ editing,
4247
+ setEditing,
4248
+ textShown: offered && (showText || text !== ""),
4249
+ textAsked: showText,
4250
+ addText: offered && !showText && text === "" ? () => setShowText(true) : void 0
4251
+ };
4252
+ }
4253
+ function useLatestValue(props) {
4254
+ const latest = React14.useRef(props.value);
4255
+ latest.current = props.value;
4256
+ return {
4257
+ get: () => latest.current,
4258
+ change: (next) => {
4259
+ latest.current = next;
4260
+ props.onChange(next);
4261
+ }
4262
+ };
4263
+ }
4264
+ function useSuggestions(lists, { get, change }) {
4265
+ const service = useTerminology2();
4266
+ return (coding) => {
4267
+ if (!coding || !service?.translate) return;
4268
+ const { system, code, version } = coding;
4269
+ const empty = codingsByList2(get(), lists).byList.flatMap(
4270
+ (c, row) => row > 0 && !c ? [row] : []
4271
+ );
4272
+ for (const row of empty)
4273
+ service.translate({
4274
+ coding: { system, code, ...version ? { version } : {} },
4275
+ targets: [lists[row]]
4276
+ }).then(([first]) => {
4277
+ if (first) change(withSuggested(get(), lists, row, first));
4278
+ }).catch(() => void 0);
4279
+ };
4280
+ }
4281
+ function CodeableConceptWidget(props) {
4282
+ const { id, value, onBlur } = props;
4283
+ const c = useConcept(props);
4284
+ const editable = !props.disabled && !props.readonly;
4285
+ const blur = () => onBlur?.(id, value);
4286
+ const latest = useLatestValue(props);
4287
+ const suggest = useSuggestions(c.lists, latest);
4288
+ const set = (row, coding) => {
4289
+ latest.change(withRow(latest.get(), c.lists, row, coding));
4290
+ if (row === 0) suggest(coding);
4291
+ };
4292
+ const editingList = c.editing !== null ? c.lists[c.editing] : void 0;
4293
+ return /* @__PURE__ */ jsx32(
4294
+ FormFieldWrapper,
4295
+ {
4296
+ inputId: id,
4297
+ label: props.label,
4298
+ description: props.description,
4299
+ required: props.required,
4300
+ error: firstError(props.rawErrors),
4301
+ children: /* @__PURE__ */ jsxs21("div", { className: "space-y-1.5", children: [
4302
+ /* @__PURE__ */ jsx32(
4303
+ FirstList,
4304
+ {
4305
+ props,
4306
+ lists: c.lists,
4307
+ coding: c.rows[0],
4308
+ set,
4309
+ blur
4310
+ }
4311
+ ),
4312
+ /* @__PURE__ */ jsxs21(
4313
+ Popover4,
4314
+ {
4315
+ open: editingList !== void 0,
4316
+ onOpenChange: (o) => {
4317
+ if (o) return;
4318
+ c.setEditing(null);
4319
+ blur();
4320
+ },
4321
+ children: [
4322
+ /* @__PURE__ */ jsx32(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx32("div", { children: /* @__PURE__ */ jsx32(
4323
+ ChipLine,
4324
+ {
4325
+ lists: c.lists,
4326
+ rows: c.rows,
4327
+ others: c.others,
4328
+ open: editable ? c.setEditing : void 0,
4329
+ set: editable ? set : void 0,
4330
+ addText: editable ? c.addText : void 0
4331
+ }
4332
+ ) }) }),
4333
+ /* @__PURE__ */ jsx32(
4334
+ PopoverContent4,
4335
+ {
4336
+ className: "w-[--radix-popover-trigger-width] min-w-72 p-0",
4337
+ align: "start",
4338
+ children: editingList && c.editing !== null && /* @__PURE__ */ jsx32(
4339
+ ListSearch,
4340
+ {
4341
+ list: editingList,
4342
+ props,
4343
+ selected: c.rows[c.editing],
4344
+ onPick: (hit) => {
4345
+ set(
4346
+ c.editing,
4347
+ toStored(hit, AS_CODING, void 0)
4348
+ );
4349
+ c.setEditing(null);
4350
+ }
4351
+ }
4352
+ )
4353
+ }
4354
+ )
4355
+ ]
4356
+ }
4357
+ ),
4358
+ c.textShown && /* @__PURE__ */ jsx32(
4359
+ FormInput,
4360
+ {
4361
+ id: `${id}-text`,
4362
+ label: "Text",
4363
+ value: c.text,
4364
+ placeholder: "In words, as the person said it",
4365
+ disabled: props.disabled,
4366
+ readOnly: props.readonly,
4367
+ onChange: (e) => latest.change(withText(latest.get(), e.target.value)),
4368
+ onBlur: blur,
4369
+ autoFocus: c.textAsked
4370
+ }
4371
+ )
4372
+ ] })
4373
+ }
4374
+ );
4375
+ }
4376
+ function FirstList({
4377
+ props,
4378
+ lists,
4379
+ coding,
4380
+ set,
4381
+ blur
4382
+ }) {
4383
+ if (!lists[0])
4384
+ return /* @__PURE__ */ jsx32("p", { className: "text-sm text-muted-foreground", children: "No list is bound to this field." });
4385
+ return /* @__PURE__ */ jsx32(
4386
+ CodedValueWidget,
4387
+ {
4388
+ ...props,
4389
+ label: "",
4390
+ description: void 0,
4391
+ rawErrors: void 0,
4392
+ propertySchema: listSchema(lists[0]),
4393
+ value: coding,
4394
+ onChange: (next) => set(0, next),
4395
+ onBlur: blur
2705
4396
  }
2706
4397
  );
2707
4398
  }
2708
4399
 
2709
4400
  // src/widgets/DateWidget.tsx
2710
4401
  import { DateInput } from "@petrarca/sonnet-ui";
2711
- import { jsx as jsx26 } from "react/jsx-runtime";
4402
+ import { jsx as jsx33 } from "react/jsx-runtime";
2712
4403
  function resolvePrecision(value) {
2713
4404
  return value === "year" || value === "month" ? value : "day";
2714
4405
  }
@@ -2723,12 +4414,13 @@ function DateWidget({
2723
4414
  label,
2724
4415
  description,
2725
4416
  placeholder,
2726
- options
4417
+ options,
4418
+ propertySchema
2727
4419
  }) {
2728
4420
  const error = firstError(rawErrors);
2729
4421
  const precision = resolvePrecision(options?.precision);
2730
4422
  const locale = typeof options?.locale === "string" ? options.locale : void 0;
2731
- return /* @__PURE__ */ jsx26(
4423
+ return /* @__PURE__ */ jsx33(
2732
4424
  FormFieldWrapper,
2733
4425
  {
2734
4426
  inputId: id,
@@ -2736,7 +4428,7 @@ function DateWidget({
2736
4428
  description,
2737
4429
  required,
2738
4430
  labelPlacement: options?.labelPlacement,
2739
- children: /* @__PURE__ */ jsx26(
4431
+ children: /* @__PURE__ */ jsx33(
2740
4432
  DateInput,
2741
4433
  {
2742
4434
  id,
@@ -2748,7 +4440,9 @@ function DateWidget({
2748
4440
  placeholder,
2749
4441
  required,
2750
4442
  disabled,
2751
- error
4443
+ error,
4444
+ min: propertySchema.formatMinimum,
4445
+ max: propertySchema.formatMaximum
2752
4446
  }
2753
4447
  )
2754
4448
  }
@@ -2757,7 +4451,7 @@ function DateWidget({
2757
4451
 
2758
4452
  // src/widgets/TimeWidget.tsx
2759
4453
  import { TimeInput } from "@petrarca/sonnet-ui";
2760
- import { jsx as jsx27 } from "react/jsx-runtime";
4454
+ import { jsx as jsx34 } from "react/jsx-runtime";
2761
4455
  function resolveGranularity(value) {
2762
4456
  return value === "second" ? "second" : "minute";
2763
4457
  }
@@ -2776,7 +4470,7 @@ function TimeWidget({
2776
4470
  }) {
2777
4471
  const error = firstError(rawErrors);
2778
4472
  const granularity = resolveGranularity(options?.granularity);
2779
- return /* @__PURE__ */ jsx27(
4473
+ return /* @__PURE__ */ jsx34(
2780
4474
  FormFieldWrapper,
2781
4475
  {
2782
4476
  inputId: id,
@@ -2784,7 +4478,7 @@ function TimeWidget({
2784
4478
  description,
2785
4479
  required,
2786
4480
  labelPlacement: options?.labelPlacement,
2787
- children: /* @__PURE__ */ jsx27(
4481
+ children: /* @__PURE__ */ jsx34(
2788
4482
  TimeInput,
2789
4483
  {
2790
4484
  id,
@@ -2804,7 +4498,7 @@ function TimeWidget({
2804
4498
 
2805
4499
  // src/widgets/DateTimeWidget.tsx
2806
4500
  import { DateTimeInput } from "@petrarca/sonnet-ui";
2807
- import { jsx as jsx28 } from "react/jsx-runtime";
4501
+ import { jsx as jsx35 } from "react/jsx-runtime";
2808
4502
  function resolveGranularity2(value) {
2809
4503
  return value === "second" ? "second" : "minute";
2810
4504
  }
@@ -2818,13 +4512,14 @@ function DateTimeWidget({
2818
4512
  rawErrors,
2819
4513
  label,
2820
4514
  description,
2821
- options
4515
+ options,
4516
+ propertySchema
2822
4517
  }) {
2823
4518
  const error = firstError(rawErrors);
2824
4519
  const granularity = resolveGranularity2(options?.granularity);
2825
4520
  const timeZone = typeof options?.timeZone === "string" ? options.timeZone : void 0;
2826
4521
  const locale = typeof options?.locale === "string" ? options.locale : void 0;
2827
- return /* @__PURE__ */ jsx28(
4522
+ return /* @__PURE__ */ jsx35(
2828
4523
  FormFieldWrapper,
2829
4524
  {
2830
4525
  inputId: id,
@@ -2832,7 +4527,7 @@ function DateTimeWidget({
2832
4527
  description,
2833
4528
  required,
2834
4529
  labelPlacement: options?.labelPlacement,
2835
- children: /* @__PURE__ */ jsx28(
4530
+ children: /* @__PURE__ */ jsx35(
2836
4531
  DateTimeInput,
2837
4532
  {
2838
4533
  idPrefix: id,
@@ -2844,7 +4539,9 @@ function DateTimeWidget({
2844
4539
  locale,
2845
4540
  required,
2846
4541
  disabled,
2847
- error
4542
+ error,
4543
+ min: propertySchema.formatMinimum,
4544
+ max: propertySchema.formatMaximum
2848
4545
  }
2849
4546
  )
2850
4547
  }
@@ -2852,20 +4549,20 @@ function DateTimeWidget({
2852
4549
  }
2853
4550
 
2854
4551
  // src/widgets/DisplayWidget.tsx
2855
- import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
4552
+ import { jsx as jsx36, jsxs as jsxs22 } from "react/jsx-runtime";
2856
4553
  function DisplayWidget({
2857
4554
  label,
2858
4555
  description
2859
4556
  }) {
2860
4557
  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 })
4558
+ return /* @__PURE__ */ jsxs22("div", { className: "space-y-1", children: [
4559
+ label && /* @__PURE__ */ jsx36("p", { className: "text-sm text-foreground", children: label }),
4560
+ description && /* @__PURE__ */ jsx36("p", { className: "text-sm text-muted-foreground", children: description })
2864
4561
  ] });
2865
4562
  }
2866
4563
 
2867
4564
  // src/widgets/OneOfWidget.tsx
2868
- import { useMemo as useMemo6 } from "react";
4565
+ import { useMemo as useMemo8 } from "react";
2869
4566
  import {
2870
4567
  getDiscriminator,
2871
4568
  getBranchValue,
@@ -2880,7 +4577,7 @@ import {
2880
4577
  SelectContent,
2881
4578
  SelectItem
2882
4579
  } from "@petrarca/sonnet-ui";
2883
- import { jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
4580
+ import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
2884
4581
  function buildChoices(branches, discriminator) {
2885
4582
  return branches.map((branch, index) => {
2886
4583
  const raw = discriminator ? getBranchValue(branch, discriminator) : void 0;
@@ -2916,15 +4613,20 @@ function BranchSelector({
2916
4613
  disabled,
2917
4614
  onSelect
2918
4615
  }) {
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)) })
4616
+ return /* @__PURE__ */ jsxs23("div", { className: "space-y-1", children: [
4617
+ label && /* @__PURE__ */ jsx37(Label, { htmlFor: id, children: label }),
4618
+ /* @__PURE__ */ jsxs23(Select, { value: activeValue, onValueChange: onSelect, disabled, children: [
4619
+ /* @__PURE__ */ jsx37(SelectTrigger, { id, children: /* @__PURE__ */ jsx37(SelectValue, { placeholder: "Select an option" }) }),
4620
+ /* @__PURE__ */ jsx37(SelectContent, { children: choices.map((c) => /* @__PURE__ */ jsx37(SelectItem, { value: c.value, children: c.label }, c.value)) })
2924
4621
  ] }),
2925
- description && /* @__PURE__ */ jsx30("p", { className: "text-sm text-muted-foreground", children: description })
4622
+ description && /* @__PURE__ */ jsx37("p", { className: "text-sm text-muted-foreground", children: description })
2926
4623
  ] });
2927
4624
  }
4625
+ function branchUiSchema(uiSchema, discriminator) {
4626
+ const fields = uiSchema?.["x-ui-fields"] ?? {};
4627
+ if (!discriminator) return fields;
4628
+ return { [discriminator]: { "x-ui-options": { hidden: true } }, ...fields };
4629
+ }
2928
4630
  function resolveActive(propertySchema, branches, choices, discriminator, data) {
2929
4631
  const index = findActiveBranch(propertySchema, data, discriminator);
2930
4632
  if (index < 0) return { value: "", label: "", subSchema: null };
@@ -2949,12 +4651,12 @@ function OneOfWidget({
2949
4651
  }) {
2950
4652
  const { renderer: Renderer, templates } = useNestedFormContext();
2951
4653
  const ContainerTemplate = templates.ObjectContainerTemplate ?? DefaultObjectContainerTemplate;
2952
- const branches = useMemo6(() => propertySchema.oneOf ?? [], [propertySchema]);
2953
- const discriminator = useMemo6(
4654
+ const branches = useMemo8(() => propertySchema.oneOf ?? [], [propertySchema]);
4655
+ const discriminator = useMemo8(
2954
4656
  () => getDiscriminator(propertySchema),
2955
4657
  [propertySchema]
2956
4658
  );
2957
- const choices = useMemo6(
4659
+ const choices = useMemo8(
2958
4660
  () => buildChoices(branches, discriminator),
2959
4661
  [branches, discriminator]
2960
4662
  );
@@ -2967,9 +4669,9 @@ function OneOfWidget({
2967
4669
  data
2968
4670
  );
2969
4671
  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(
4672
+ const subUiSchema = branchUiSchema(uiSchema, discriminator);
4673
+ return /* @__PURE__ */ jsxs23("div", { className: "space-y-4", children: [
4674
+ /* @__PURE__ */ jsx37(
2973
4675
  BranchSelector,
2974
4676
  {
2975
4677
  id,
@@ -2981,14 +4683,14 @@ function OneOfWidget({
2981
4683
  onSelect: (selected) => onChange(branchChangeData(choices, branches, discriminator, selected))
2982
4684
  }
2983
4685
  ),
2984
- active.subSchema && /* @__PURE__ */ jsx30(
4686
+ active.subSchema && /* @__PURE__ */ jsx37(
2985
4687
  ContainerTemplate,
2986
4688
  {
2987
4689
  label: active.label,
2988
4690
  description: void 0,
2989
4691
  variant: options?.variant || "section",
2990
4692
  defaultOpen: true,
2991
- children: /* @__PURE__ */ jsx30(
4693
+ children: /* @__PURE__ */ jsx37(
2992
4694
  Renderer,
2993
4695
  {
2994
4696
  schema: active.subSchema,
@@ -3006,8 +4708,304 @@ function OneOfWidget({
3006
4708
  ] });
3007
4709
  }
3008
4710
 
4711
+ // src/widgets/RadioWidget.tsx
4712
+ import { RadioGroup, RadioGroupItem } from "@petrarca/sonnet-ui";
4713
+ import { cn as cn14 } from "@petrarca/sonnet-core";
4714
+ import { jsx as jsx38, jsxs as jsxs24 } from "react/jsx-runtime";
4715
+ function RadioWidget({
4716
+ id,
4717
+ name,
4718
+ value,
4719
+ onChange,
4720
+ onBlur,
4721
+ propertySchema,
4722
+ required,
4723
+ disabled,
4724
+ readonly,
4725
+ rawErrors,
4726
+ label,
4727
+ description,
4728
+ options
4729
+ }) {
4730
+ const choices = buildSelectOptions(propertySchema, options?.enumNames ?? {});
4731
+ const horizontal = options?.orientation === "horizontal";
4732
+ return /* @__PURE__ */ jsx38(
4733
+ FormFieldWrapper,
4734
+ {
4735
+ inputId: id,
4736
+ label,
4737
+ description,
4738
+ error: firstError(rawErrors),
4739
+ required,
4740
+ compact: options?.compact,
4741
+ children: /* @__PURE__ */ jsx38(
4742
+ RadioGroup,
4743
+ {
4744
+ id,
4745
+ name,
4746
+ value: value === void 0 || value === null ? "" : String(value),
4747
+ onValueChange: (v) => onChange(v),
4748
+ onBlur: () => onBlur?.(id, value),
4749
+ disabled: disabled || readonly,
4750
+ required,
4751
+ className: cn14(horizontal && "flex flex-wrap gap-4"),
4752
+ children: choices.map((c) => /* @__PURE__ */ jsxs24("label", { className: "flex items-center gap-2 text-sm", children: [
4753
+ /* @__PURE__ */ jsx38(RadioGroupItem, { value: c.value, id: `${id}-${c.value}` }),
4754
+ c.label
4755
+ ] }, c.value))
4756
+ }
4757
+ )
4758
+ }
4759
+ );
4760
+ }
4761
+
4762
+ // src/widgets/CheckboxGroupWidget.tsx
4763
+ import { Checkbox as Checkbox2 } from "@petrarca/sonnet-ui";
4764
+ import { cn as cn15 } from "@petrarca/sonnet-core";
4765
+ import { jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
4766
+ function CheckboxGroupWidget({
4767
+ id,
4768
+ value,
4769
+ onChange,
4770
+ onBlur,
4771
+ propertySchema,
4772
+ required,
4773
+ disabled,
4774
+ readonly,
4775
+ rawErrors,
4776
+ label,
4777
+ description,
4778
+ options
4779
+ }) {
4780
+ const choices = buildSelectOptions(propertySchema, options?.enumNames ?? {});
4781
+ const chosen = new Set(Array.isArray(value) ? value.map(String) : []);
4782
+ const toggle = (v, on) => {
4783
+ const next = choices.map((c) => c.value).filter((c) => c === v ? on : chosen.has(c));
4784
+ onChange(next);
4785
+ };
4786
+ return /* @__PURE__ */ jsx39(
4787
+ FormFieldWrapper,
4788
+ {
4789
+ inputId: id,
4790
+ label,
4791
+ description,
4792
+ error: firstError(rawErrors),
4793
+ required,
4794
+ compact: options?.compact,
4795
+ children: /* @__PURE__ */ jsx39(
4796
+ "div",
4797
+ {
4798
+ id,
4799
+ role: "group",
4800
+ className: cn15(
4801
+ "grid gap-2",
4802
+ options?.orientation === "horizontal" && "flex flex-wrap gap-4"
4803
+ ),
4804
+ onBlur: () => onBlur?.(id, value),
4805
+ children: choices.map((c) => /* @__PURE__ */ jsxs25("label", { className: "flex items-center gap-2 text-sm", children: [
4806
+ /* @__PURE__ */ jsx39(
4807
+ Checkbox2,
4808
+ {
4809
+ id: `${id}-${c.value}`,
4810
+ checked: chosen.has(c.value),
4811
+ onCheckedChange: (on) => toggle(c.value, on === true),
4812
+ disabled: disabled || readonly
4813
+ }
4814
+ ),
4815
+ c.label
4816
+ ] }, c.value))
4817
+ }
4818
+ )
4819
+ }
4820
+ );
4821
+ }
4822
+
4823
+ // src/widgets/ChoiceWithTextWidget.tsx
4824
+ import { jsx as jsx40 } from "react/jsx-runtime";
4825
+ var ROW_LAYOUT = {
4826
+ rows: [{ fields: ["choice", "text"], widths: ["md", "full"] }]
4827
+ };
4828
+ function textTitleOf(prop) {
4829
+ const text = prop.properties?.text ?? prop.allOf?.[0]?.then?.properties?.text;
4830
+ return text?.title;
4831
+ }
4832
+ function containerUiSchema(props) {
4833
+ const fields = props.uiSchema?.["x-ui-fields"] ?? {};
4834
+ const textTitle = textTitleOf(props.propertySchema);
4835
+ const layout = props.uiSchema?.["x-ui-layout"];
4836
+ return {
4837
+ ...props.uiSchema,
4838
+ "x-ui-layout": layout ?? ROW_LAYOUT,
4839
+ "x-ui-fields": {
4840
+ ...fields,
4841
+ choice: { "x-ui-title": false, ...fields.choice },
4842
+ text: {
4843
+ "x-ui-title": false,
4844
+ ...textTitle ? { "x-ui-placeholder": textTitle } : {},
4845
+ ...fields.text
4846
+ }
4847
+ }
4848
+ };
4849
+ }
4850
+ function ChoiceWithTextWidget(props) {
4851
+ return /* @__PURE__ */ jsx40(
4852
+ FormFieldWrapper,
4853
+ {
4854
+ inputId: props.id,
4855
+ label: props.label,
4856
+ description: props.description,
4857
+ required: props.required,
4858
+ compact: props.options?.compact,
4859
+ children: /* @__PURE__ */ jsx40(
4860
+ ObjectWidget,
4861
+ {
4862
+ ...props,
4863
+ uiSchema: containerUiSchema(props),
4864
+ options: { ...props.options, variant: "inline" }
4865
+ }
4866
+ )
4867
+ }
4868
+ );
4869
+ }
4870
+
4871
+ // src/widgets/AbsentReasonsWidget.tsx
4872
+ import {
4873
+ Button as Button9,
4874
+ IconButton as IconButton2,
4875
+ Select as Select2,
4876
+ SelectContent as SelectContent2,
4877
+ SelectItem as SelectItem2,
4878
+ SelectTrigger as SelectTrigger2,
4879
+ SelectValue as SelectValue2
4880
+ } from "@petrarca/sonnet-ui";
4881
+ import { Plus as Plus5, X as X7 } from "lucide-react";
4882
+ import { jsx as jsx41, jsxs as jsxs26 } from "react/jsx-runtime";
4883
+ function Pick({
4884
+ label,
4885
+ value,
4886
+ items,
4887
+ onChange,
4888
+ className
4889
+ }) {
4890
+ return /* @__PURE__ */ jsxs26(Select2, { value: value || void 0, onValueChange: onChange, children: [
4891
+ /* @__PURE__ */ jsx41(SelectTrigger2, { "aria-label": label, className, children: /* @__PURE__ */ jsx41(SelectValue2, { placeholder: label.toLowerCase() }) }),
4892
+ /* @__PURE__ */ jsx41(SelectContent2, { children: items.map((i) => /* @__PURE__ */ jsx41(SelectItem2, { value: i.value, children: i.label }, i.value)) })
4893
+ ] });
4894
+ }
4895
+ function questionsOf(props) {
4896
+ return Object.entries(props.schema.properties ?? {}).filter(([key, p]) => key !== props.name && p.type !== "null").map(([key, p]) => ({ value: key, label: p.title ?? key }));
4897
+ }
4898
+ function ReasonRow({
4899
+ questionKey,
4900
+ reason,
4901
+ given,
4902
+ questions,
4903
+ reasons,
4904
+ locked,
4905
+ set
4906
+ }) {
4907
+ const { [questionKey]: _, ...rest } = given;
4908
+ return /* @__PURE__ */ jsxs26("div", { className: "group/row flex items-center gap-1.5 px-1.5 py-1", children: [
4909
+ /* @__PURE__ */ jsx41(
4910
+ Pick,
4911
+ {
4912
+ label: "Question",
4913
+ value: questionKey,
4914
+ items: questions.filter(
4915
+ (q) => q.value === questionKey || !(q.value in given)
4916
+ ),
4917
+ onChange: (next) => set({ ...rest, [next]: reason }),
4918
+ className: "h-7 min-w-0 flex-1 text-xs"
4919
+ }
4920
+ ),
4921
+ /* @__PURE__ */ jsx41(
4922
+ Pick,
4923
+ {
4924
+ label: "Reason",
4925
+ value: reason,
4926
+ items: reasons,
4927
+ onChange: (r) => set({ ...given, [questionKey]: r }),
4928
+ className: "h-7 w-40 shrink-0 text-xs"
4929
+ }
4930
+ ),
4931
+ !locked && /* @__PURE__ */ jsx41(
4932
+ IconButton2,
4933
+ {
4934
+ label: "Answer it after all",
4935
+ revealOnHover: true,
4936
+ onClick: () => set(rest),
4937
+ children: /* @__PURE__ */ jsx41(X7, {})
4938
+ }
4939
+ )
4940
+ ] });
4941
+ }
4942
+ function reasonsState(props) {
4943
+ const { value, readonly, disabled, options } = props;
4944
+ const additional = props.propertySchema.additionalProperties;
4945
+ const reasons = buildSelectOptions(
4946
+ additional ?? {},
4947
+ options?.enumNames ?? {}
4948
+ );
4949
+ const given = value && typeof value === "object" ? value : {};
4950
+ const questions = questionsOf(props);
4951
+ const free = questions.filter((q) => !(q.value in given));
4952
+ const locked = readonly || disabled;
4953
+ return {
4954
+ reasons,
4955
+ given,
4956
+ questions,
4957
+ free,
4958
+ locked,
4959
+ canAdd: !locked && free.length > 0 && reasons.length > 0
4960
+ };
4961
+ }
4962
+ function AbsentReasonsWidget(props) {
4963
+ const { id, onChange, label, rawErrors, options } = props;
4964
+ const { reasons, given, questions, free, locked, canAdd } = reasonsState(props);
4965
+ const set = (next) => onChange(Object.keys(next).length ? next : void 0);
4966
+ return /* @__PURE__ */ jsx41(
4967
+ FormFieldWrapper,
4968
+ {
4969
+ inputId: id,
4970
+ label,
4971
+ error: firstError(rawErrors),
4972
+ compact: options?.compact,
4973
+ children: /* @__PURE__ */ jsxs26("div", { className: "space-y-1.5", children: [
4974
+ Object.keys(given).length > 0 && /* @__PURE__ */ jsx41("div", { className: "rounded-md border divide-y", children: Object.entries(given).map(([key, reason]) => /* @__PURE__ */ jsx41(
4975
+ ReasonRow,
4976
+ {
4977
+ questionKey: key,
4978
+ reason,
4979
+ given,
4980
+ questions,
4981
+ reasons,
4982
+ locked,
4983
+ set
4984
+ },
4985
+ key
4986
+ )) }),
4987
+ canAdd && /* @__PURE__ */ jsxs26(
4988
+ Button9,
4989
+ {
4990
+ type: "button",
4991
+ variant: "ghost",
4992
+ size: "sm",
4993
+ className: "h-7 gap-1 px-2 text-xs",
4994
+ onClick: () => set({ ...given, [free[0].value]: reasons[0].value }),
4995
+ children: [
4996
+ /* @__PURE__ */ jsx41(Plus5, { className: "h-3.5 w-3.5" }),
4997
+ "Leave a question unanswered"
4998
+ ]
4999
+ }
5000
+ )
5001
+ ] })
5002
+ }
5003
+ );
5004
+ }
5005
+
3009
5006
  // src/widgets/widgetResolver.ts
3010
5007
  import {
5008
+ bindingOf as bindingOf3,
3011
5009
  isNullableBoolean as isNullableBoolean2,
3012
5010
  isOneOf,
3013
5011
  extractEnumValues as extractEnumValues2,
@@ -3084,20 +5082,45 @@ function resolveFromUiWidget(uiWidget, propertySchema, registry) {
3084
5082
  function isObjectArraySchema(schema) {
3085
5083
  return schema.type === "array" && schema.items?.type === "object" && !!schema.items?.properties;
3086
5084
  }
5085
+ function isStringArraySchema(schema) {
5086
+ return schema.type === "array" && schema.items?.type === "string" && !Array.isArray(schema.items?.enum);
5087
+ }
5088
+ function isStringMapSchema(schema) {
5089
+ const extra = schema.additionalProperties;
5090
+ return schema.type === "object" && !schema.properties && typeof extra === "object" && extra !== null && extra.type === "string";
5091
+ }
5092
+ function isEnumArraySchema(schema) {
5093
+ return schema.type === "array" && Array.isArray(schema.items?.enum);
5094
+ }
5095
+ function isCodedShape(schema) {
5096
+ return schema.type === "string" || schema.type === "array" || !!schema.anyOf;
5097
+ }
3087
5098
  function registryLookup(registry, widgetName) {
3088
5099
  return registry[widgetName] || registry["TextInputWidget"];
3089
5100
  }
5101
+ function objectWidgetName(schema) {
5102
+ if (schema.properties) return "ObjectWidget";
5103
+ if (isStringMapSchema(schema)) return "KeyValueWidget";
5104
+ return "JsonEditorWidget";
5105
+ }
5106
+ function arrayWidgetName(schema) {
5107
+ if (isObjectArraySchema(schema)) return "ArrayWidget";
5108
+ if (isEnumArraySchema(schema)) return "SelectWidget";
5109
+ if (isStringArraySchema(schema)) return "TagsInputWidget";
5110
+ return null;
5111
+ }
3090
5112
  function resolveFromSchemaStructure(schema, registry) {
3091
5113
  if (isOneOf(schema)) return registryLookup(registry, "OneOfWidget");
3092
5114
  if (isNullableBoolean2(schema))
3093
5115
  return registryLookup(registry, "CheckboxWidget");
3094
5116
  if (extractEnumValues2(schema).length > 0)
3095
5117
  return registryLookup(registry, "SelectWidget");
3096
- if (schema.type === "object" && schema.properties)
3097
- return registryLookup(registry, "ObjectWidget");
3098
- if (isObjectArraySchema(schema))
3099
- return registryLookup(registry, "ArrayWidget");
3100
- return null;
5118
+ if (bindingOf3(schema) && isCodedShape(schema))
5119
+ return registryLookup(registry, "CodedValueWidget");
5120
+ if (schema.type === "object")
5121
+ return registryLookup(registry, objectWidgetName(schema));
5122
+ const arrayWidget = arrayWidgetName(schema);
5123
+ return arrayWidget ? registryLookup(registry, arrayWidget) : null;
3101
5124
  }
3102
5125
  function resolveFromTypeFormat(schema, registry) {
3103
5126
  const type = getFieldType(schema);
@@ -3128,15 +5151,22 @@ var DEFAULT_WIDGETS = {
3128
5151
  SelectWidget,
3129
5152
  TagsInputWidget,
3130
5153
  JsonEditorWidget,
5154
+ KeyValueWidget,
3131
5155
  EntitySelectWidget,
3132
5156
  ObjectWidget,
3133
5157
  ArrayWidget,
3134
5158
  QuantityWidget,
5159
+ CodedValueWidget,
5160
+ CodeableConceptWidget,
3135
5161
  DateWidget,
3136
5162
  TimeWidget,
3137
5163
  DateTimeWidget,
3138
5164
  DisplayWidget,
3139
5165
  OneOfWidget,
5166
+ RadioWidget,
5167
+ CheckboxGroupWidget,
5168
+ ChoiceWithTextWidget,
5169
+ AbsentReasonsWidget,
3140
5170
  // Lowercase aliases for x-ui-widget convenience
3141
5171
  text: TextInputWidget,
3142
5172
  textarea: TextareaWidget,
@@ -3145,21 +5175,28 @@ var DEFAULT_WIDGETS = {
3145
5175
  select: SelectWidget,
3146
5176
  tags: TagsInputWidget,
3147
5177
  "json-editor": JsonEditorWidget,
5178
+ "key-value": KeyValueWidget,
3148
5179
  "entity-select": EntitySelectWidget,
3149
5180
  object: ObjectWidget,
3150
5181
  array: ArrayWidget,
3151
5182
  quantity: QuantityWidget,
5183
+ "coded-value": CodedValueWidget,
5184
+ "codeable-concept": CodeableConceptWidget,
3152
5185
  date: DateWidget,
3153
5186
  time: TimeWidget,
3154
5187
  "date-time": DateTimeWidget,
3155
5188
  display: DisplayWidget,
3156
- "one-of": OneOfWidget
5189
+ "one-of": OneOfWidget,
5190
+ radio: RadioWidget,
5191
+ "checkbox-group": CheckboxGroupWidget,
5192
+ "choice-with-text": ChoiceWithTextWidget,
5193
+ "absent-reasons": AbsentReasonsWidget
3157
5194
  };
3158
5195
 
3159
5196
  // src/templates/HorizontalArrayItemTemplate.tsx
3160
5197
  import { Trash2 as Trash22, ChevronUp as ChevronUp2, ChevronDown as ChevronDown4 } from "lucide-react";
3161
- import { Button as Button6 } from "@petrarca/sonnet-ui";
3162
- import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
5198
+ import { Button as Button10 } from "@petrarca/sonnet-ui";
5199
+ import { jsx as jsx42, jsxs as jsxs27 } from "react/jsx-runtime";
3163
5200
  function HorizontalArrayItemTemplate({
3164
5201
  children,
3165
5202
  index,
@@ -3183,7 +5220,7 @@ function HorizontalArrayItemTemplate({
3183
5220
  );
3184
5221
  const gap = resolvedLayout.gap ?? 8;
3185
5222
  const alignItems = resolvedLayout.compact ? "center" : "start";
3186
- return /* @__PURE__ */ jsxs19(
5223
+ return /* @__PURE__ */ jsxs27(
3187
5224
  "div",
3188
5225
  {
3189
5226
  "data-fid": fid,
@@ -3195,9 +5232,9 @@ function HorizontalArrayItemTemplate({
3195
5232
  },
3196
5233
  children: [
3197
5234
  children,
3198
- orderable && /* @__PURE__ */ jsxs19("div", { className: "flex flex-col items-center justify-center", children: [
3199
- /* @__PURE__ */ jsx31(
3200
- Button6,
5235
+ orderable && /* @__PURE__ */ jsxs27("div", { className: "flex flex-col items-center justify-center", children: [
5236
+ /* @__PURE__ */ jsx42(
5237
+ Button10,
3201
5238
  {
3202
5239
  type: "button",
3203
5240
  variant: "ghost",
@@ -3206,11 +5243,11 @@ function HorizontalArrayItemTemplate({
3206
5243
  onClick: onMoveUp,
3207
5244
  title: "Move up",
3208
5245
  className: "h-4 w-8 p-0",
3209
- children: /* @__PURE__ */ jsx31(ChevronUp2, { className: "h-3 w-3" })
5246
+ children: /* @__PURE__ */ jsx42(ChevronUp2, { className: "h-3 w-3" })
3210
5247
  }
3211
5248
  ),
3212
- /* @__PURE__ */ jsx31(
3213
- Button6,
5249
+ /* @__PURE__ */ jsx42(
5250
+ Button10,
3214
5251
  {
3215
5252
  type: "button",
3216
5253
  variant: "ghost",
@@ -3219,12 +5256,12 @@ function HorizontalArrayItemTemplate({
3219
5256
  onClick: onMoveDown,
3220
5257
  title: "Move down",
3221
5258
  className: "h-4 w-8 p-0",
3222
- children: /* @__PURE__ */ jsx31(ChevronDown4, { className: "h-3 w-3" })
5259
+ children: /* @__PURE__ */ jsx42(ChevronDown4, { className: "h-3 w-3" })
3223
5260
  }
3224
5261
  )
3225
5262
  ] }),
3226
- canRemove && /* @__PURE__ */ jsx31(
3227
- Button6,
5263
+ canRemove && /* @__PURE__ */ jsx42(
5264
+ Button10,
3228
5265
  {
3229
5266
  type: "button",
3230
5267
  variant: "ghost",
@@ -3233,7 +5270,7 @@ function HorizontalArrayItemTemplate({
3233
5270
  onClick: onRemove,
3234
5271
  title: "Remove item",
3235
5272
  className: "text-muted-foreground hover:text-destructive h-8 w-8 p-0",
3236
- children: /* @__PURE__ */ jsx31(Trash22, { className: "h-3.5 w-3.5" })
5273
+ children: /* @__PURE__ */ jsx42(Trash22, { className: "h-3.5 w-3.5" })
3237
5274
  }
3238
5275
  )
3239
5276
  ]
@@ -3242,49 +5279,49 @@ function HorizontalArrayItemTemplate({
3242
5279
  }
3243
5280
 
3244
5281
  // src/templates/TabsGroupTemplate.tsx
3245
- import { useState as useState7 } from "react";
5282
+ import { useState as useState14 } from "react";
3246
5283
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@petrarca/sonnet-ui";
3247
5284
 
3248
5285
  // src/templates/GroupErrorBadge.tsx
3249
5286
  import { AlertCircle } from "lucide-react";
3250
- import { jsx as jsx32 } from "react/jsx-runtime";
5287
+ import { jsx as jsx43 } from "react/jsx-runtime";
3251
5288
  function GroupErrorBadge({
3252
5289
  count
3253
5290
  }) {
3254
5291
  if (count === 0) return null;
3255
5292
  const label = count === 1 ? "1 field needs attention" : `${count} fields need attention`;
3256
- return /* @__PURE__ */ jsx32(
5293
+ return /* @__PURE__ */ jsx43(
3257
5294
  "span",
3258
5295
  {
3259
5296
  role: "img",
3260
5297
  "aria-label": label,
3261
5298
  title: label,
3262
5299
  className: "inline-flex shrink-0 text-destructive",
3263
- children: /* @__PURE__ */ jsx32(AlertCircle, { className: "h-3.5 w-3.5" })
5300
+ children: /* @__PURE__ */ jsx43(AlertCircle, { className: "h-3.5 w-3.5" })
3264
5301
  }
3265
5302
  );
3266
5303
  }
3267
5304
 
3268
5305
  // src/templates/TabsGroupTemplate.tsx
3269
- import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
5306
+ import { jsx as jsx44, jsxs as jsxs28 } from "react/jsx-runtime";
3270
5307
  function TabsGroupTemplate({
3271
5308
  groups
3272
5309
  }) {
3273
- const [active, setActive] = useState7(groups[0].key);
5310
+ const [active, setActive] = useState14(groups[0].key);
3274
5311
  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: [
5312
+ return /* @__PURE__ */ jsxs28(Tabs, { value, onValueChange: setActive, children: [
5313
+ /* @__PURE__ */ jsx44(TabsList, { className: "mb-4", children: groups.map((g) => /* @__PURE__ */ jsxs28(TabsTrigger, { value: g.key, className: "gap-1.5", children: [
3277
5314
  g.label,
3278
- /* @__PURE__ */ jsx33(GroupErrorBadge, { count: g.errorCount })
5315
+ /* @__PURE__ */ jsx44(GroupErrorBadge, { count: g.errorCount })
3279
5316
  ] }, g.key)) }),
3280
- groups.map((g) => /* @__PURE__ */ jsx33(TabsContent, { value: g.key, className: "space-y-4", children: g.content }, g.key))
5317
+ groups.map((g) => /* @__PURE__ */ jsx44(TabsContent, { value: g.key, className: "space-y-4", children: g.content }, g.key))
3281
5318
  ] });
3282
5319
  }
3283
5320
 
3284
5321
  // src/templates/StepperGroupTemplate.tsx
3285
- import { useState as useState8 } from "react";
5322
+ import { useState as useState15 } from "react";
3286
5323
  import {
3287
- Button as Button7,
5324
+ Button as Button11,
3288
5325
  Stepper,
3289
5326
  StepperList,
3290
5327
  StepperItem,
@@ -3296,47 +5333,47 @@ import {
3296
5333
  StepperPrev,
3297
5334
  StepperNext
3298
5335
  } from "@petrarca/sonnet-ui";
3299
- import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
5336
+ import { jsx as jsx45, jsxs as jsxs29 } from "react/jsx-runtime";
3300
5337
  function StepperGroupTemplate({
3301
5338
  groups
3302
5339
  }) {
3303
- const [active, setActive] = useState8(groups[0].key);
5340
+ const [active, setActive] = useState15(groups[0].key);
3304
5341
  const value = groups.some((g) => g.key === active) ? active : groups[0].key;
3305
5342
  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: [
5343
+ return /* @__PURE__ */ jsxs29(Stepper, { value, onValueChange: setActive, children: [
5344
+ /* @__PURE__ */ jsx45(StepperList, { className: "mb-4 gap-2", children: groups.map((g) => /* @__PURE__ */ jsxs29(StepperItem, { value: g.key, children: [
5345
+ /* @__PURE__ */ jsxs29(StepperTrigger, { children: [
5346
+ /* @__PURE__ */ jsx45(StepperIndicator, {}),
5347
+ /* @__PURE__ */ jsxs29(StepperTitle, { className: "inline-flex items-center gap-1.5", children: [
3311
5348
  g.label,
3312
- /* @__PURE__ */ jsx34(GroupErrorBadge, { count: g.errorCount })
5349
+ /* @__PURE__ */ jsx45(GroupErrorBadge, { count: g.errorCount })
3313
5350
  ] })
3314
5351
  ] }),
3315
- /* @__PURE__ */ jsx34(StepperSeparator, {})
5352
+ /* @__PURE__ */ jsx45(StepperSeparator, {})
3316
5353
  ] }, 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" }) })
5354
+ groups.map((g) => /* @__PURE__ */ jsx45(StepperContent, { value: g.key, className: "space-y-4", children: g.content }, g.key)),
5355
+ /* @__PURE__ */ jsxs29("div", { className: "mt-4 flex justify-between", children: [
5356
+ /* @__PURE__ */ jsx45(StepperPrev, { asChild: true, children: /* @__PURE__ */ jsx45(Button11, { variant: "outline", size: "sm", children: "Back" }) }),
5357
+ !isLast && /* @__PURE__ */ jsx45(StepperNext, { asChild: true, children: /* @__PURE__ */ jsx45(Button11, { size: "sm", children: "Next" }) })
3321
5358
  ] })
3322
5359
  ] });
3323
5360
  }
3324
5361
 
3325
5362
  // src/templates/SectionsGroupTemplate.tsx
3326
- import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
5363
+ import { jsx as jsx46, jsxs as jsxs30 } from "react/jsx-runtime";
3327
5364
  function SectionsGroupTemplate({
3328
5365
  groups
3329
5366
  }) {
3330
- return /* @__PURE__ */ jsx35("div", { className: "space-y-6", children: groups.map((g) => /* @__PURE__ */ jsxs22(
5367
+ return /* @__PURE__ */ jsx46("div", { className: "space-y-6", children: groups.map((g) => /* @__PURE__ */ jsxs30(
3331
5368
  "fieldset",
3332
5369
  {
3333
5370
  className: "rounded-md border border-border/60 px-3 pb-3 pt-1",
3334
5371
  children: [
3335
- /* @__PURE__ */ jsxs22("legend", { className: "inline-flex items-center gap-1.5 px-1 text-sm font-medium text-muted-foreground", children: [
5372
+ /* @__PURE__ */ jsxs30("legend", { className: "inline-flex items-center gap-1.5 px-1 text-sm font-medium text-muted-foreground", children: [
3336
5373
  g.label,
3337
- /* @__PURE__ */ jsx35(GroupErrorBadge, { count: g.errorCount })
5374
+ /* @__PURE__ */ jsx46(GroupErrorBadge, { count: g.errorCount })
3338
5375
  ] }),
3339
- /* @__PURE__ */ jsx35("div", { className: "space-y-4", children: g.content })
5376
+ /* @__PURE__ */ jsx46("div", { className: "space-y-4", children: g.content })
3340
5377
  ]
3341
5378
  },
3342
5379
  g.key
@@ -3360,13 +5397,15 @@ function groupTemplateKey(variant) {
3360
5397
  }
3361
5398
 
3362
5399
  // src/FormFieldRenderer.tsx
5400
+ import { useMemo as useMemo9 } from "react";
3363
5401
  import { warnLog } from "@petrarca/sonnet-core";
3364
5402
  import {
3365
5403
  getFieldTitle,
3366
5404
  getFieldHelpText,
3367
- getFieldPlaceholder
5405
+ getFieldPlaceholder,
5406
+ liftNullableMember
3368
5407
  } from "@petrarca/sonnet-core/schema";
3369
- import { jsx as jsx36 } from "react/jsx-runtime";
5408
+ import { jsx as jsx47 } from "react/jsx-runtime";
3370
5409
  function mergeFieldOptions(globalOptions, property, uiSchema) {
3371
5410
  const xWidgetOptions = property["x-ui-options"] || {};
3372
5411
  const fieldOptions = uiSchema?.["x-ui-options"] || {};
@@ -3393,7 +5432,7 @@ function applyFieldWidth(widget, uiSchema, mergedOptions) {
3393
5432
  }
3394
5433
  const width = candidate && candidate in FIELD_WIDTH_CLASS ? candidate : "full";
3395
5434
  if (width === "full") return widget;
3396
- return /* @__PURE__ */ jsx36("div", { className: FIELD_WIDTH_CLASS[width], children: widget });
5435
+ return /* @__PURE__ */ jsx47("div", { className: FIELD_WIDTH_CLASS[width], children: widget });
3397
5436
  }
3398
5437
  function resolvePresentationText(property, uiSchema) {
3399
5438
  return {
@@ -3409,7 +5448,7 @@ function resolveFieldFlags(fieldOptions, globalOptions, disabled, readOnly) {
3409
5448
  }
3410
5449
  function FormFieldRenderer({
3411
5450
  fieldName,
3412
- property,
5451
+ property: declared,
3413
5452
  value,
3414
5453
  onChange,
3415
5454
  onBlur,
@@ -3424,6 +5463,7 @@ function FormFieldRenderer({
3424
5463
  idPrefix = "field",
3425
5464
  rawErrors
3426
5465
  }) {
5466
+ const property = useMemo9(() => liftNullableMember(declared), [declared]);
3427
5467
  const label = getFieldTitle(fieldName, property);
3428
5468
  const { description, placeholder } = resolvePresentationText(
3429
5469
  property,
@@ -3444,7 +5484,7 @@ function FormFieldRenderer({
3444
5484
  uiSchema?.["x-ui-title"],
3445
5485
  globalOptions.label
3446
5486
  );
3447
- const widget = /* @__PURE__ */ jsx36(
5487
+ const widget = /* @__PURE__ */ jsx47(
3448
5488
  Widget,
3449
5489
  {
3450
5490
  id: `${idPrefix}-${fieldName}`,
@@ -3471,19 +5511,213 @@ function FormFieldRenderer({
3471
5511
  }
3472
5512
 
3473
5513
  // src/hooks/useFormEngine.ts
3474
- import { useState as useState9, useEffect as useEffect3, useMemo as useMemo7, useCallback as useCallback7, useRef as useRef5 } from "react";
5514
+ import { useState as useState17, useEffect as useEffect8, useMemo as useMemo11, useCallback as useCallback8, useRef as useRef8 } from "react";
3475
5515
  import { isEqual } from "@petrarca/sonnet-core";
3476
5516
  import {
3477
- getSchemaProperties,
5517
+ isNullableBoolean as isNullableBoolean3,
5518
+ getSchemaProperties as getSchemaProperties4,
3478
5519
  getFieldDefault,
3479
5520
  applySchemaDefaults as applySchemaDefaults2,
3480
5521
  resolveSchema,
3481
5522
  resolveRefs,
3482
5523
  validateFormData,
5524
+ stripFormItemIds,
5525
+ mergeFieldErrors,
3483
5526
  diffFormData,
3484
5527
  ensureFormItemIds
3485
5528
  } from "@petrarca/sonnet-core/schema";
3486
5529
 
5530
+ // src/validators/runValidators.ts
5531
+ import { getSchemaProperties as getSchemaProperties2 } from "@petrarca/sonnet-core/schema";
5532
+
5533
+ // src/validators/shared.ts
5534
+ import { getSchemaProperties } from "@petrarca/sonnet-core/schema";
5535
+ function toRefs(v) {
5536
+ if (v === void 0) return [];
5537
+ const arr = Array.isArray(v) ? v : [v];
5538
+ return arr.map((item) => typeof item === "string" ? { name: item } : item);
5539
+ }
5540
+ function escapeToken(token) {
5541
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
5542
+ }
5543
+ function fieldPointer(fieldName) {
5544
+ return `/${escapeToken(fieldName)}`;
5545
+ }
5546
+ function resolveFieldRefs(fieldName, schema, uiSchema) {
5547
+ const property = getSchemaProperties(schema)[fieldName] ?? {};
5548
+ return toRefs(
5549
+ uiSchema[fieldName]?.["x-ui-validate"] ?? property["x-ui-validate"]
5550
+ );
5551
+ }
5552
+ function toFieldErrors(result, pointer) {
5553
+ if (result == null || result === "") return [];
5554
+ if (typeof result === "string") return [{ pointer, message: result }];
5555
+ if (Array.isArray(result)) return result;
5556
+ return [];
5557
+ }
5558
+
5559
+ // src/validators/runValidators.ts
5560
+ function isKnownName(registry, name) {
5561
+ return !!(registry.field?.[name] || registry.asyncField?.[name] || registry.form?.[name] || registry.asyncForm?.[name]);
5562
+ }
5563
+ function unknownValidator(kind, name, registry) {
5564
+ const registered = [
5565
+ ...Object.keys(registry.field ?? {}),
5566
+ ...Object.keys(registry.asyncField ?? {}),
5567
+ ...Object.keys(registry.form ?? {}),
5568
+ ...Object.keys(registry.asyncForm ?? {})
5569
+ ];
5570
+ throw new Error(
5571
+ `unknown ${kind} validator '${name}' (registered: [${registered.join(", ")}])`
5572
+ );
5573
+ }
5574
+ function runFieldRefs(fieldName, data, registry, uiSchema, schema, out) {
5575
+ const refs = resolveFieldRefs(fieldName, schema, uiSchema);
5576
+ if (refs.length === 0) return;
5577
+ const pointer = fieldPointer(fieldName);
5578
+ const value = data[fieldName];
5579
+ for (const ref of refs) {
5580
+ if (!isKnownName(registry, ref.name)) {
5581
+ unknownValidator("field", ref.name, registry);
5582
+ }
5583
+ const fn = registry.field?.[ref.name];
5584
+ if (!fn) continue;
5585
+ const ctx = {
5586
+ formData: data,
5587
+ path: pointer,
5588
+ args: ref.args
5589
+ };
5590
+ out.push(...toFieldErrors(fn(value, ctx), pointer));
5591
+ }
5592
+ }
5593
+ function runFormRefs(data, registry, schema, out) {
5594
+ for (const ref of toRefs(schema["x-ui-validate"])) {
5595
+ if (!isKnownName(registry, ref.name)) {
5596
+ unknownValidator("form", ref.name, registry);
5597
+ }
5598
+ const fn = registry.form?.[ref.name];
5599
+ if (!fn) continue;
5600
+ const ctx = { formData: data, path: "", args: ref.args };
5601
+ out.push(...fn(data, ctx));
5602
+ }
5603
+ }
5604
+ function runValidators(schema, data, registry, uiSchema, includeFormValidators) {
5605
+ const out = [];
5606
+ if (!registry) return out;
5607
+ for (const fieldName of Object.keys(getSchemaProperties2(schema))) {
5608
+ runFieldRefs(fieldName, data, registry, uiSchema, schema, out);
5609
+ }
5610
+ if (includeFormValidators) {
5611
+ runFormRefs(data, registry, schema, out);
5612
+ }
5613
+ return out;
5614
+ }
5615
+
5616
+ // src/validators/useAsyncValidators.ts
5617
+ import { useEffect as useEffect7, useMemo as useMemo10, useRef as useRef7, useState as useState16 } from "react";
5618
+ import { getSchemaProperties as getSchemaProperties3 } from "@petrarca/sonnet-core/schema";
5619
+ function keyOf(value) {
5620
+ try {
5621
+ return JSON.stringify(value) ?? "undefined";
5622
+ } catch {
5623
+ return String(value);
5624
+ }
5625
+ }
5626
+ function collectFieldCandidates(schema, data, registry, uiSchema, out) {
5627
+ for (const fieldName of Object.keys(getSchemaProperties3(schema))) {
5628
+ const refs = resolveFieldRefs(fieldName, schema, uiSchema);
5629
+ const pointer = fieldPointer(fieldName);
5630
+ const value = data[fieldName];
5631
+ for (const ref of refs) {
5632
+ const fn = registry[ref.name];
5633
+ if (!fn) continue;
5634
+ const ctx = {
5635
+ formData: data,
5636
+ path: pointer,
5637
+ args: ref.args
5638
+ };
5639
+ out.push({
5640
+ id: `asyncField:${pointer}:${ref.name}`,
5641
+ key: keyOf(value),
5642
+ pointer,
5643
+ run: () => fn(value, ctx)
5644
+ });
5645
+ }
5646
+ }
5647
+ }
5648
+ function collectFormCandidates(schema, data, registry, out) {
5649
+ for (const ref of toRefs(schema["x-ui-validate"])) {
5650
+ const fn = registry[ref.name];
5651
+ if (!fn) continue;
5652
+ const ctx = { formData: data, path: "", args: ref.args };
5653
+ const basis = ref.dependsOn ? ref.dependsOn.map((f) => keyOf(data[f])).join("|") : keyOf(data);
5654
+ out.push({
5655
+ id: `asyncForm:${ref.name}`,
5656
+ key: basis,
5657
+ pointer: "",
5658
+ run: () => fn(data, ctx)
5659
+ });
5660
+ }
5661
+ }
5662
+ function collectCandidates(schema, data, registry, uiSchema, isRoot) {
5663
+ const out = [];
5664
+ if (registry.asyncField) {
5665
+ collectFieldCandidates(schema, data, registry.asyncField, uiSchema, out);
5666
+ }
5667
+ if (registry.asyncForm && isRoot) {
5668
+ collectFormCandidates(schema, data, registry.asyncForm, out);
5669
+ }
5670
+ return out;
5671
+ }
5672
+ function useAsyncValidators(schema, data, registry, uiSchema, isRoot) {
5673
+ const [entries, setEntries] = useState16({});
5674
+ const handledKey = useRef7({});
5675
+ const candidates = registry && (registry.asyncField || registry.asyncForm) ? collectCandidates(schema, data, registry, uiSchema, isRoot) : [];
5676
+ const liveIds = new Set(candidates.map((c) => c.id));
5677
+ const signature = candidates.map((c) => `${c.id}=${c.key}`).join(";");
5678
+ useEffect7(() => {
5679
+ for (const c of candidates) {
5680
+ if (handledKey.current[c.id] === c.key) continue;
5681
+ handledKey.current[c.id] = c.key;
5682
+ setEntries((e) => ({
5683
+ ...e,
5684
+ [c.id]: { key: c.key, pending: true, errors: [] }
5685
+ }));
5686
+ const settle = (errors) => {
5687
+ if (handledKey.current[c.id] !== c.key) return;
5688
+ setEntries((e) => ({
5689
+ ...e,
5690
+ [c.id]: { key: c.key, pending: false, errors }
5691
+ }));
5692
+ };
5693
+ c.run().then((r) => settle(toFieldErrors(r, c.pointer))).catch(() => settle([]));
5694
+ }
5695
+ }, [signature]);
5696
+ const liveKey = [...liveIds].sort().join(",");
5697
+ useEffect7(() => {
5698
+ setEntries((e) => {
5699
+ const kept = Object.entries(e).filter(([id]) => liveIds.has(id));
5700
+ return kept.length === Object.keys(e).length ? e : Object.fromEntries(kept);
5701
+ });
5702
+ for (const id of Object.keys(handledKey.current)) {
5703
+ if (!liveIds.has(id)) delete handledKey.current[id];
5704
+ }
5705
+ }, [liveKey]);
5706
+ let isValidating = false;
5707
+ const errorList = [];
5708
+ for (const c of candidates) {
5709
+ const entry = entries[c.id];
5710
+ if (!entry || entry.key !== c.key || entry.pending) {
5711
+ isValidating = true;
5712
+ continue;
5713
+ }
5714
+ errorList.push(...entry.errors);
5715
+ }
5716
+ const errorSig = errorList.map((e) => `${e.pointer}\0${e.message}`).join("|");
5717
+ const asyncErrors = useMemo10(() => errorList, [errorSig]);
5718
+ return { asyncErrors, isValidating };
5719
+ }
5720
+
3487
5721
  // src/hooks/orderProperties.ts
3488
5722
  function orderProperties(properties, orderArray) {
3489
5723
  const allEntries = Object.entries(properties);
@@ -3500,9 +5734,14 @@ function orderProperties(properties, orderArray) {
3500
5734
  }
3501
5735
 
3502
5736
  // src/hooks/useFormEngine.ts
5737
+ var COMMITTED_FORMATS = /* @__PURE__ */ new Set(["date", "time", "date-time"]);
5738
+ function isCommittedFormat(property) {
5739
+ return property.format !== void 0 && COMMITTED_FORMATS.has(property.format);
5740
+ }
3503
5741
  function isImmediateField(fieldName, properties) {
3504
5742
  const property = properties[fieldName];
3505
- return property?.type === "boolean" || property?.enum !== void 0;
5743
+ if (!property) return false;
5744
+ return property.type === "boolean" || isNullableBoolean3(property) || isCommittedFormat(property) || property.enum !== void 0 || property.items?.enum !== void 0 || property["x-ui-options"]?.immediate === true;
3506
5745
  }
3507
5746
  function computeBaselineUpdate(newData, originalData, initialized, schemaPropertyNames) {
3508
5747
  const newCount = Object.keys(newData).length;
@@ -3527,6 +5766,11 @@ function computeBaselineUpdate(newData, originalData, initialized, schemaPropert
3527
5766
  }
3528
5767
  return null;
3529
5768
  }
5769
+ function computeAllErrors(args) {
5770
+ const { schema, data, customSync, asyncErrors, extraErrors } = args;
5771
+ const base = validateFormData(schema, stripFormItemIds(data)).fieldErrors;
5772
+ return mergeFieldErrors(base, customSync, asyncErrors, extraErrors ?? []);
5773
+ }
3530
5774
  function useFormEngine({
3531
5775
  schema,
3532
5776
  data,
@@ -3541,51 +5785,54 @@ function useFormEngine({
3541
5785
  deferChanges = false,
3542
5786
  deferStateUpdates = false,
3543
5787
  allowPristineSave = false,
3544
- refResolver
5788
+ refResolver,
5789
+ extraErrors,
5790
+ validators,
5791
+ isRoot = true
3545
5792
  }) {
3546
- const refResolvedSchema = useMemo7(
5793
+ const refResolvedSchema = useMemo11(
3547
5794
  () => resolveRefs(schema, refResolver),
3548
5795
  [schema, refResolver]
3549
5796
  );
3550
- const seed = useState9(
5797
+ const seed = useState17(
3551
5798
  () => ensureFormItemIds(
3552
5799
  applySchemaDefaults2(data, refResolvedSchema),
3553
5800
  refResolvedSchema
3554
5801
  )
3555
5802
  )[0];
3556
- const [originalData, setOriginalData] = useState9(
5803
+ const [originalData, setOriginalData] = useState17(
3557
5804
  () => seed
3558
5805
  );
3559
- const [originalDataInitialized, setOriginalDataInitialized] = useState9(false);
3560
- const [nonSchemaData, setNonSchemaData] = useState9(
5806
+ const [originalDataInitialized, setOriginalDataInitialized] = useState17(false);
5807
+ const [nonSchemaData, setNonSchemaData] = useState17(
3561
5808
  () => {
3562
- const props = getSchemaProperties(refResolvedSchema);
5809
+ const props = getSchemaProperties4(refResolvedSchema);
3563
5810
  const names = Object.keys(props);
3564
5811
  return Object.fromEntries(
3565
5812
  Object.entries(data).filter(([k]) => !names.includes(k))
3566
5813
  );
3567
5814
  }
3568
5815
  );
3569
- const [formData, setFormData] = useState9(() => seed);
3570
- const formDataRef = useRef5(formData);
5816
+ const [formData, setFormData] = useState17(() => seed);
5817
+ const formDataRef = useRef8(formData);
3571
5818
  formDataRef.current = formData;
3572
- const [pendingChanges, setPendingChanges] = useState9({});
3573
- const resolvedSchema = useMemo7(
5819
+ const [pendingChanges, setPendingChanges] = useState17({});
5820
+ const resolvedSchema = useMemo11(
3574
5821
  () => resolveSchema(refResolvedSchema, formData),
3575
5822
  [refResolvedSchema, formData]
3576
5823
  );
3577
- const orderedProperties = useMemo7(() => {
3578
- const properties2 = getSchemaProperties(resolvedSchema);
5824
+ const orderedProperties = useMemo11(() => {
5825
+ const properties2 = getSchemaProperties4(resolvedSchema);
3579
5826
  const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
3580
5827
  const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
3581
5828
  return orderProperties(properties2, orderArray);
3582
5829
  }, [resolvedSchema, uiSchema]);
3583
- const [initialDefaultsSent, setInitialDefaultsSent] = useState9(false);
3584
- const [edited, setEdited] = useState9({});
3585
- const [touched, setTouched] = useState9({});
3586
- const [submitAttempted, setSubmitAttempted] = useState9(false);
3587
- const properties = getSchemaProperties(resolvedSchema);
3588
- useEffect3(() => {
5830
+ const [initialDefaultsSent, setInitialDefaultsSent] = useState17(false);
5831
+ const edited = useRef8(/* @__PURE__ */ new Set());
5832
+ const [touched, setTouched] = useState17({});
5833
+ const [submitAttempted, setSubmitAttempted] = useState17(false);
5834
+ const properties = getSchemaProperties4(resolvedSchema);
5835
+ useEffect8(() => {
3589
5836
  if (!initialDefaultsSent) {
3590
5837
  const hasDefaults = Object.entries(properties).some(
3591
5838
  ([fieldName, property]) => data[fieldName] === void 0 && getFieldDefault(property) !== void 0
@@ -3596,7 +5843,7 @@ function useFormEngine({
3596
5843
  setInitialDefaultsSent(true);
3597
5844
  }
3598
5845
  }, [initialDefaultsSent, properties, data, formData, onChange]);
3599
- useEffect3(() => {
5846
+ useEffect8(() => {
3600
5847
  const currentPropertyNames = Object.keys(properties);
3601
5848
  const keysToRemove = Object.keys(formData).filter(
3602
5849
  (k) => !currentPropertyNames.includes(k)
@@ -3610,8 +5857,23 @@ function useFormEngine({
3610
5857
  if (onChange) onChange(cleanedData);
3611
5858
  }
3612
5859
  }, [properties, formData, onChange]);
3613
- const prevDataRef = useRef5(data);
3614
- useEffect3(() => {
5860
+ const shownFields = useRef8(
5861
+ new Set(Object.keys(getSchemaProperties4(refResolvedSchema)))
5862
+ );
5863
+ useEffect8(() => {
5864
+ const names = Object.keys(properties);
5865
+ const appeared = names.filter((n) => !shownFields.current.has(n));
5866
+ shownFields.current = new Set(names);
5867
+ if (appeared.length === 0) return;
5868
+ const filled = applySchemaDefaults2(formData, {
5869
+ properties: Object.fromEntries(appeared.map((n) => [n, properties[n]]))
5870
+ });
5871
+ if (isEqual(filled, formData)) return;
5872
+ setFormData(filled);
5873
+ if (onChange) onChange(filled);
5874
+ }, [properties, formData, onChange]);
5875
+ const prevDataRef = useRef8(data);
5876
+ useEffect8(() => {
3615
5877
  if (isEqual(prevDataRef.current, data)) return;
3616
5878
  prevDataRef.current = data;
3617
5879
  const newData = ensureFormItemIds(
@@ -3641,32 +5903,51 @@ function useFormEngine({
3641
5903
  originalData,
3642
5904
  originalDataInitialized
3643
5905
  ]);
3644
- const hasChanges = useMemo7(
5906
+ const hasChanges = useMemo11(
3645
5907
  () => !isEqual(formData, originalData),
3646
5908
  [formData, originalData]
3647
5909
  );
3648
- const displayData = useMemo7(() => {
5910
+ const displayData = useMemo11(() => {
3649
5911
  if (deferStateUpdates && Object.keys(pendingChanges).length > 0) {
3650
5912
  return { ...formData, ...pendingChanges };
3651
5913
  }
3652
5914
  return formData;
3653
5915
  }, [formData, pendingChanges, deferStateUpdates]);
3654
- const validation = useMemo7(
3655
- () => validateFormData(resolvedSchema, displayData),
3656
- [resolvedSchema, displayData]
5916
+ const customSync = useMemo11(
5917
+ () => runValidators(resolvedSchema, displayData, validators, uiSchema, isRoot),
5918
+ [resolvedSchema, displayData, validators, uiSchema, isRoot]
5919
+ );
5920
+ const { asyncErrors, isValidating } = useAsyncValidators(
5921
+ resolvedSchema,
5922
+ displayData,
5923
+ validators,
5924
+ uiSchema,
5925
+ isRoot
3657
5926
  );
5927
+ const validation = useMemo11(() => {
5928
+ const merged = computeAllErrors({
5929
+ schema: resolvedSchema,
5930
+ data: displayData,
5931
+ customSync,
5932
+ asyncErrors,
5933
+ extraErrors
5934
+ });
5935
+ return {
5936
+ fieldErrors: merged,
5937
+ errors: merged.map((e) => e.message),
5938
+ isValid: merged.length === 0
5939
+ };
5940
+ }, [resolvedSchema, displayData, customSync, asyncErrors, extraErrors]);
3658
5941
  const { isValid, fieldErrors } = validation;
3659
- const onValidationChangeRef = useRef5(onValidationChange);
5942
+ const onValidationChangeRef = useRef8(onValidationChange);
3660
5943
  onValidationChangeRef.current = onValidationChange;
3661
- useEffect3(() => {
5944
+ useEffect8(() => {
3662
5945
  onValidationChangeRef.current?.(validation.errors, validation.isValid);
3663
5946
  }, [validation]);
3664
- const handleFieldChange = useCallback7(
5947
+ const handleFieldChange = useCallback8(
3665
5948
  (fieldName, value) => {
3666
5949
  if (readOnly) return;
3667
- setEdited(
3668
- (prev) => prev[fieldName] ? prev : { ...prev, [fieldName]: true }
3669
- );
5950
+ edited.current.add(fieldName);
3670
5951
  const immediate = isImmediateField(fieldName, properties);
3671
5952
  if (deferStateUpdates && !immediate) {
3672
5953
  setPendingChanges((prev) => ({ ...prev, [fieldName]: value }));
@@ -3689,7 +5970,7 @@ function useFormEngine({
3689
5970
  properties
3690
5971
  ]
3691
5972
  );
3692
- const flushPendingField = useCallback7(
5973
+ const flushPendingField = useCallback8(
3693
5974
  (fieldName) => {
3694
5975
  if (!deferStateUpdates) return;
3695
5976
  if (pendingChanges[fieldName] === void 0) return;
@@ -3704,10 +5985,10 @@ function useFormEngine({
3704
5985
  },
3705
5986
  [showActions, deferStateUpdates, pendingChanges, formData, onChange]
3706
5987
  );
3707
- const handleFieldBlur = useCallback7(
5988
+ const handleFieldBlur = useCallback8(
3708
5989
  (fieldName) => {
3709
5990
  if (readOnly) return;
3710
- if (edited[fieldName]) {
5991
+ if (edited.current.has(fieldName)) {
3711
5992
  setTouched(
3712
5993
  (prev) => prev[fieldName] ? prev : { ...prev, [fieldName]: true }
3713
5994
  );
@@ -3725,7 +6006,6 @@ function useFormEngine({
3725
6006
  },
3726
6007
  [
3727
6008
  readOnly,
3728
- edited,
3729
6009
  pendingChanges,
3730
6010
  deferChanges,
3731
6011
  deferStateUpdates,
@@ -3735,13 +6015,26 @@ function useFormEngine({
3735
6015
  flushPendingField
3736
6016
  ]
3737
6017
  );
3738
- const handleSave = useCallback7(() => {
6018
+ const handleSave = useCallback8(() => {
3739
6019
  setSubmitAttempted(true);
3740
6020
  if (!hasChanges && !allowPristineSave) return;
6021
+ if (isValidating) return;
3741
6022
  const finalData = deferStateUpdates && Object.keys(pendingChanges).length > 0 ? { ...formData, ...pendingChanges } : formData;
3742
6023
  const completeData = { ...nonSchemaData, ...finalData };
3743
- const validation2 = validateFormData(resolvedSchema, finalData);
3744
- if (validation2.isValid && onUpdate) {
6024
+ const saveErrors = computeAllErrors({
6025
+ schema: resolvedSchema,
6026
+ data: finalData,
6027
+ customSync: runValidators(
6028
+ resolvedSchema,
6029
+ finalData,
6030
+ validators,
6031
+ uiSchema,
6032
+ isRoot
6033
+ ),
6034
+ asyncErrors,
6035
+ extraErrors
6036
+ });
6037
+ if (saveErrors.length === 0 && onUpdate) {
3745
6038
  const changed = {};
3746
6039
  for (const key of Object.keys(finalData)) {
3747
6040
  if (!isEqual(finalData[key], originalData[key])) {
@@ -3761,12 +6054,18 @@ function useFormEngine({
3761
6054
  pendingChanges,
3762
6055
  deferStateUpdates,
3763
6056
  nonSchemaData,
3764
- onUpdate
6057
+ onUpdate,
6058
+ extraErrors,
6059
+ asyncErrors,
6060
+ isValidating,
6061
+ uiSchema,
6062
+ validators,
6063
+ isRoot
3765
6064
  ]);
3766
- const handleCancel = useCallback7(() => {
6065
+ const handleCancel = useCallback8(() => {
3767
6066
  setFormData({ ...originalData });
3768
6067
  setPendingChanges({});
3769
- setEdited({});
6068
+ edited.current = /* @__PURE__ */ new Set();
3770
6069
  setTouched({});
3771
6070
  setSubmitAttempted(false);
3772
6071
  onCancel?.();
@@ -3778,6 +6077,7 @@ function useFormEngine({
3778
6077
  orderedProperties,
3779
6078
  hasChanges,
3780
6079
  isValid,
6080
+ isValidating,
3781
6081
  fieldErrors,
3782
6082
  touched,
3783
6083
  submitAttempted,
@@ -3793,30 +6093,7 @@ import {
3793
6093
  useValidationDisplay,
3794
6094
  useSaveWhenInvalid
3795
6095
  } from "@petrarca/sonnet-core/config";
3796
- import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
3797
- function stripFids(data) {
3798
- const result = {};
3799
- for (const [key, value] of Object.entries(data)) {
3800
- if (Array.isArray(value)) {
3801
- result[key] = value.map((item) => {
3802
- if (item !== null && typeof item === "object" && !Array.isArray(item)) {
3803
- const rest = Object.fromEntries(
3804
- Object.entries(item).filter(
3805
- ([k]) => k !== FORM_ITEM_ID_FIELD2
3806
- )
3807
- );
3808
- return stripFids(rest);
3809
- }
3810
- return item;
3811
- });
3812
- } else if (value !== null && typeof value === "object") {
3813
- result[key] = stripFids(value);
3814
- } else {
3815
- result[key] = value;
3816
- }
3817
- }
3818
- return result;
3819
- }
6096
+ import { jsx as jsx48, jsxs as jsxs31 } from "react/jsx-runtime";
3820
6097
  function resolveFlagDefaults(props) {
3821
6098
  return {
3822
6099
  disabled: props.disabled ?? false,
@@ -3838,18 +6115,20 @@ function resolveValueDefaults(props) {
3838
6115
  uiSchema: props.uiSchema ?? {}
3839
6116
  };
3840
6117
  }
3841
- function useWrappedCallbacks(onChange, onBlur, onUpdate) {
3842
- const wrappedOnChange = useCallback8(
3843
- (d) => onChange?.(stripFids(d)),
3844
- [onChange]
6118
+ var keepFids = (data) => data;
6119
+ function useWrappedCallbacks(onChange, onBlur, onUpdate, isRootForm) {
6120
+ const strip = isRootForm ? stripFormItemIds2 : keepFids;
6121
+ const wrappedOnChange = useCallback9(
6122
+ (d) => onChange?.(strip(d)),
6123
+ [onChange, strip]
3845
6124
  );
3846
- const wrappedOnBlur = useCallback8(
3847
- (fieldName, d) => onBlur?.(fieldName, stripFids(d)),
3848
- [onBlur]
6125
+ const wrappedOnBlur = useCallback9(
6126
+ (fieldName, d) => onBlur?.(fieldName, strip(d)),
6127
+ [onBlur, strip]
3849
6128
  );
3850
- const wrappedOnUpdate = useCallback8(
3851
- (d, changed, diff) => onUpdate?.(stripFids(d), stripFids(changed), diff),
3852
- [onUpdate]
6129
+ const wrappedOnUpdate = useCallback9(
6130
+ (d, changed, diff) => onUpdate?.(strip(d), strip(changed), diff),
6131
+ [onUpdate, strip]
3853
6132
  );
3854
6133
  return {
3855
6134
  wrappedOnChange: onChange ? wrappedOnChange : void 0,
@@ -3860,7 +6139,7 @@ function useWrappedCallbacks(onChange, onBlur, onUpdate) {
3860
6139
  function buildFieldMap(fields) {
3861
6140
  const map = /* @__PURE__ */ new Map();
3862
6141
  for (const field of fields) {
3863
- if (React13.isValidElement(field) && field.key != null) {
6142
+ if (React18.isValidElement(field) && field.key != null) {
3864
6143
  map.set(String(field.key), field);
3865
6144
  }
3866
6145
  }
@@ -3879,12 +6158,12 @@ function resolveGridColumns(row, fieldCount) {
3879
6158
  }
3880
6159
  function wrapInGroup(group, key, content) {
3881
6160
  if (!group) return content;
3882
- return /* @__PURE__ */ jsxs23(
6161
+ return /* @__PURE__ */ jsxs31(
3883
6162
  "fieldset",
3884
6163
  {
3885
6164
  className: "rounded-md border border-border/60 px-3 pb-3 pt-1",
3886
6165
  children: [
3887
- /* @__PURE__ */ jsx37("legend", { className: "px-1 text-sm font-medium text-muted-foreground", children: group }),
6166
+ /* @__PURE__ */ jsx48("legend", { className: "px-1 text-sm font-medium text-muted-foreground", children: group }),
3888
6167
  content
3889
6168
  ]
3890
6169
  },
@@ -3906,7 +6185,7 @@ function buildRowElement(row, fieldMap, defaultRowGap) {
3906
6185
  gridTemplateColumns: resolveGridColumns(row, rowFields.length),
3907
6186
  gap: `${rowGap}px`
3908
6187
  };
3909
- const inner = /* @__PURE__ */ jsx37("div", { style, children: rowFields }, `row:${rowKey}`);
6188
+ const inner = /* @__PURE__ */ jsx48("div", { style, children: rowFields }, `row:${rowKey}`);
3910
6189
  return {
3911
6190
  element: wrapInGroup(row.group, rowKey, inner),
3912
6191
  placed: row.fields
@@ -3915,7 +6194,7 @@ function buildRowElement(row, fieldMap, defaultRowGap) {
3915
6194
  function collectUnplacedFields(fields, placed) {
3916
6195
  const result = [];
3917
6196
  for (const field of fields) {
3918
- if (React13.isValidElement(field) && field.key != null && !placed.has(String(field.key))) {
6197
+ if (React18.isValidElement(field) && field.key != null && !placed.has(String(field.key))) {
3919
6198
  result.push(field);
3920
6199
  }
3921
6200
  }
@@ -3940,8 +6219,11 @@ function layoutFieldsInRows(fields, rowDefs, defaultRowGap) {
3940
6219
  );
3941
6220
  return elements.concat(collectUnplacedFields(fields, placed));
3942
6221
  }
3943
- function useRootFormData(ownData) {
3944
- return useContext2(NestedFormContext)?.rootData ?? ownData;
6222
+ function rootFormData(nestedContext, ownData) {
6223
+ return nestedContext?.rootData ?? ownData;
6224
+ }
6225
+ function rootSubmitAttempted(nestedContext, ownSubmitAttempted) {
6226
+ return ownSubmitAttempted || (nestedContext?.submitAttempted ?? false);
3945
6227
  }
3946
6228
  function blurHandlerNeeded({
3947
6229
  deferStateUpdates,
@@ -3960,6 +6242,19 @@ function pointerRoot(pointer) {
3960
6242
  const token = pointer.split("/")[1] ?? "";
3961
6243
  return token.replace(/~1/g, "/").replace(/~0/g, "~");
3962
6244
  }
6245
+ function fieldRawErrors(fieldName, fieldErrors, extraErrors, gated) {
6246
+ const forField = fieldErrors.filter(
6247
+ (e) => pointerRoot(e.pointer) === fieldName
6248
+ );
6249
+ if (forField.length === 0) return void 0;
6250
+ const extraMessages = new Set(
6251
+ (extraErrors ?? []).filter((e) => pointerRoot(e.pointer) === fieldName).map((e) => e.message)
6252
+ );
6253
+ const always = forField.filter((e) => extraMessages.has(e.message)).map((e) => e.message);
6254
+ const rest = gated ? forField.filter((e) => !extraMessages.has(e.message)).map((e) => e.message) : [];
6255
+ const all = [...always, ...rest];
6256
+ return all.length > 0 ? all : void 0;
6257
+ }
3963
6258
  function groupErrorCount(tab, fieldErrors) {
3964
6259
  if (fieldErrors.length === 0) return 0;
3965
6260
  const owned = new Set(tab.rows.flatMap((row) => row.fields));
@@ -3992,11 +6287,28 @@ function HiddenErrorNotice({
3992
6287
  count
3993
6288
  }) {
3994
6289
  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" }),
6290
+ return /* @__PURE__ */ jsxs31("p", { className: "flex items-center gap-1.5 text-sm text-destructive", children: [
6291
+ /* @__PURE__ */ jsx48(AlertCircle2, { className: "h-4 w-4 shrink-0" }),
3997
6292
  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
6293
  ] });
3999
6294
  }
6295
+ function FormLevelErrors({
6296
+ messages
6297
+ }) {
6298
+ if (messages.length === 0) return null;
6299
+ return /* @__PURE__ */ jsx48("div", { className: "space-y-1", children: messages.map((msg, i) => /* @__PURE__ */ jsxs31(
6300
+ "p",
6301
+ {
6302
+ role: "alert",
6303
+ className: "flex items-center gap-1.5 text-sm text-destructive",
6304
+ children: [
6305
+ /* @__PURE__ */ jsx48(AlertCircle2, { className: "h-4 w-4 shrink-0" }),
6306
+ msg
6307
+ ]
6308
+ },
6309
+ i
6310
+ )) });
6311
+ }
4000
6312
  function layoutFieldsInGroups(fields, groupDefs, defaultRowGap, fieldErrors, formData, GroupTemplate) {
4001
6313
  const fieldMap = buildFieldMap(fields);
4002
6314
  const placed = /* @__PURE__ */ new Set();
@@ -4015,7 +6327,7 @@ function layoutFieldsInGroups(fields, groupDefs, defaultRowGap, fieldErrors, for
4015
6327
  content: b.rows,
4016
6328
  errorCount: groupErrorCount(groupDefs[b.index], fieldErrors)
4017
6329
  }));
4018
- return [/* @__PURE__ */ jsx37(GroupTemplate, { groups }, "form:groups"), ...unplaced];
6330
+ return [/* @__PURE__ */ jsx48(GroupTemplate, { groups }, "form:groups"), ...unplaced];
4019
6331
  }
4020
6332
  function FormContainer({
4021
6333
  displayContents,
@@ -4028,22 +6340,25 @@ function FormContainer({
4028
6340
  disabled,
4029
6341
  hasChanges,
4030
6342
  isValid,
6343
+ isValidating,
4031
6344
  saveWhenInvalid,
4032
6345
  saveLabel,
4033
6346
  cancelLabel,
4034
6347
  showCancel,
4035
6348
  leadingActions,
4036
6349
  hiddenErrorCount,
6350
+ formLevelErrors,
4037
6351
  children
4038
6352
  }) {
4039
6353
  if (displayContents) {
4040
- return /* @__PURE__ */ jsx37("div", { style: { display: "contents" }, children });
6354
+ return /* @__PURE__ */ jsx48("div", { style: { display: "contents" }, children });
4041
6355
  }
4042
- return /* @__PURE__ */ jsxs23(SimpleStack, { gap: "md", children: [
6356
+ return /* @__PURE__ */ jsxs31(SimpleStack, { gap: "md", children: [
4043
6357
  children,
4044
- showExtraProperties && /* @__PURE__ */ jsx37(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
4045
- /* @__PURE__ */ jsx37(HiddenErrorNotice, { count: hiddenErrorCount }),
4046
- showActions && /* @__PURE__ */ jsx37(
6358
+ showExtraProperties && /* @__PURE__ */ jsx48(ExtraPropertiesCard_default, { properties: formData, schema: resolvedSchema }),
6359
+ /* @__PURE__ */ jsx48(HiddenErrorNotice, { count: hiddenErrorCount }),
6360
+ /* @__PURE__ */ jsx48(FormLevelErrors, { messages: formLevelErrors }),
6361
+ showActions && /* @__PURE__ */ jsx48(
4047
6362
  FormActions,
4048
6363
  {
4049
6364
  onSave: handleSave,
@@ -4051,6 +6366,7 @@ function FormContainer({
4051
6366
  disabled,
4052
6367
  hasChanges,
4053
6368
  isValid,
6369
+ isValidating,
4054
6370
  saveWhenInvalid,
4055
6371
  saveLabel,
4056
6372
  cancelLabel,
@@ -4070,9 +6386,12 @@ function JsonSchemaFormRenderer(props) {
4070
6386
  onUpdate,
4071
6387
  onCancel,
4072
6388
  templates,
6389
+ validators,
4073
6390
  leadingActions,
4074
6391
  idPrefix = "field"
4075
6392
  } = props;
6393
+ const nestedFormContext = useContext2(NestedFormContext);
6394
+ const isRootForm = nestedFormContext === null;
4076
6395
  const {
4077
6396
  disabled,
4078
6397
  readOnly,
@@ -4085,7 +6404,7 @@ function JsonSchemaFormRenderer(props) {
4085
6404
  allowPristineSave
4086
6405
  } = resolveFlagDefaults(props);
4087
6406
  const { saveLabel, cancelLabel, widgets, uiSchema } = resolveValueDefaults(props);
4088
- const { wrappedOnChange, wrappedOnBlur, wrappedOnUpdate } = useWrappedCallbacks(onChange, onBlur, onUpdate);
6407
+ const { wrappedOnChange, wrappedOnBlur, wrappedOnUpdate } = useWrappedCallbacks(onChange, onBlur, onUpdate, isRootForm);
4089
6408
  const {
4090
6409
  formData,
4091
6410
  displayData,
@@ -4093,6 +6412,7 @@ function JsonSchemaFormRenderer(props) {
4093
6412
  orderedProperties,
4094
6413
  hasChanges,
4095
6414
  isValid,
6415
+ isValidating,
4096
6416
  fieldErrors,
4097
6417
  touched,
4098
6418
  submitAttempted,
@@ -4114,40 +6434,49 @@ function JsonSchemaFormRenderer(props) {
4114
6434
  deferChanges,
4115
6435
  deferStateUpdates,
4116
6436
  allowPristineSave,
4117
- refResolver: props.refResolver
6437
+ refResolver: props.refResolver,
6438
+ extraErrors: props.extraErrors,
6439
+ validators,
6440
+ isRoot: isRootForm
4118
6441
  });
4119
6442
  const layoutLabelPlacement = uiSchema["x-ui-layout"]?.labelPlacement;
4120
- const globalOptions = useMemo8(
6443
+ const globalOptions = useMemo12(
4121
6444
  () => ({
4122
6445
  ...layoutLabelPlacement ? { labelPlacement: layoutLabelPlacement } : {},
4123
6446
  ...uiSchema["x-ui-globalOptions"] || {}
4124
6447
  }),
4125
6448
  [layoutLabelPlacement, uiSchema]
4126
6449
  );
4127
- const resolvedTemplates = useMemo8(
6450
+ const resolvedTemplates = useMemo12(
4128
6451
  () => ({ ...DEFAULT_TEMPLATES, ...templates }),
4129
6452
  [templates]
4130
6453
  );
4131
- const nestedRenderer = useCallback8(
4132
- (props2) => /* @__PURE__ */ jsx37(
6454
+ const nestedRenderer = useCallback9(
6455
+ (props2) => /* @__PURE__ */ jsx48(
4133
6456
  JsonSchemaFormRenderer,
4134
6457
  {
4135
6458
  ...props2,
4136
6459
  templates: resolvedTemplates,
6460
+ validators,
4137
6461
  showActions: false,
4138
6462
  showExtraProperties: false
4139
6463
  }
4140
6464
  ),
4141
- [resolvedTemplates]
6465
+ [resolvedTemplates, validators]
4142
6466
  );
4143
- const rootData = useRootFormData(displayData);
4144
- const contextValue = useMemo8(
6467
+ const rootData = rootFormData(nestedFormContext, displayData);
6468
+ const effectiveSubmitAttempted = rootSubmitAttempted(
6469
+ nestedFormContext,
6470
+ submitAttempted
6471
+ );
6472
+ const contextValue = useMemo12(
4145
6473
  () => ({
4146
6474
  renderer: nestedRenderer,
4147
6475
  templates: resolvedTemplates,
4148
- rootData
6476
+ rootData,
6477
+ submitAttempted: effectiveSubmitAttempted
4149
6478
  }),
4150
- [nestedRenderer, resolvedTemplates, rootData]
6479
+ [nestedRenderer, resolvedTemplates, rootData, effectiveSubmitAttempted]
4151
6480
  );
4152
6481
  const validationDisplay = useValidationDisplay(props.validationDisplay);
4153
6482
  const saveWhenInvalid = useSaveWhenInvalid(props.saveWhenInvalid);
@@ -4157,21 +6486,21 @@ function JsonSchemaFormRenderer(props) {
4157
6486
  hasOnBlur: !!onBlur,
4158
6487
  validationDisplay
4159
6488
  });
4160
- const showFieldErrors = useCallback8(
6489
+ const showFieldErrors = useCallback9(
4161
6490
  (fieldName) => fieldErrorsVisible(
4162
6491
  validationDisplay,
4163
- submitAttempted,
6492
+ effectiveSubmitAttempted,
4164
6493
  touched[fieldName]
4165
6494
  ),
4166
- [validationDisplay, submitAttempted, touched]
6495
+ [validationDisplay, effectiveSubmitAttempted, touched]
4167
6496
  );
4168
- const fields = useMemo8(
6497
+ const fields = useMemo12(
4169
6498
  () => orderedProperties.flatMap(([fieldName, property]) => {
4170
6499
  const rule = uiSchema[fieldName]?.["x-ui-rule"] ?? property["x-ui-rule"];
4171
6500
  const { visible, enabled } = evaluateUiRule(rule, rootData);
4172
6501
  if (!visible) return [];
4173
6502
  return [
4174
- /* @__PURE__ */ jsx37(
6503
+ /* @__PURE__ */ jsx48(
4175
6504
  FormFieldRenderer,
4176
6505
  {
4177
6506
  fieldName,
@@ -4188,7 +6517,12 @@ function JsonSchemaFormRenderer(props) {
4188
6517
  globalOptions,
4189
6518
  formData: displayData,
4190
6519
  idPrefix,
4191
- rawErrors: showFieldErrors(fieldName) ? fieldErrors.filter((e) => pointerRoot(e.pointer) === fieldName).map((e) => e.message) : void 0
6520
+ rawErrors: fieldRawErrors(
6521
+ fieldName,
6522
+ fieldErrors,
6523
+ props.extraErrors,
6524
+ showFieldErrors(fieldName)
6525
+ )
4192
6526
  },
4193
6527
  fieldName
4194
6528
  )
@@ -4209,6 +6543,7 @@ function JsonSchemaFormRenderer(props) {
4209
6543
  needsBlurHandler,
4210
6544
  showFieldErrors,
4211
6545
  fieldErrors,
6546
+ props.extraErrors,
4212
6547
  handleFieldChange,
4213
6548
  handleFieldBlur
4214
6549
  ]
@@ -4221,7 +6556,7 @@ function JsonSchemaFormRenderer(props) {
4221
6556
  resolvedTemplates,
4222
6557
  layoutConfig?.variant
4223
6558
  );
4224
- const layoutFields = useMemo8(() => {
6559
+ const layoutFields = useMemo12(() => {
4225
6560
  if (groupDefs && groupDefs.length > 0) {
4226
6561
  return layoutFieldsInGroups(
4227
6562
  fields,
@@ -4246,13 +6581,17 @@ function JsonSchemaFormRenderer(props) {
4246
6581
  rootData,
4247
6582
  groupTemplate
4248
6583
  ]);
4249
- const hiddenErrorCount = useMemo8(() => {
6584
+ const hiddenErrorCount = useMemo12(() => {
4250
6585
  if (!groupDefs?.length || fieldErrors.length === 0) return 0;
4251
6586
  const hidden = hiddenGroupFieldNames(groupDefs, rootData);
4252
6587
  if (hidden.size === 0) return 0;
4253
6588
  return fieldErrors.filter((e) => hidden.has(pointerRoot(e.pointer))).length;
4254
6589
  }, [groupDefs, fieldErrors, rootData]);
4255
- return /* @__PURE__ */ jsx37(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx37(
6590
+ const formLevelErrors = useMemo12(() => {
6591
+ if (validationDisplay !== "always" && !submitAttempted) return [];
6592
+ return fieldErrors.filter((e) => e.pointer === "").map((e) => e.message);
6593
+ }, [fieldErrors, validationDisplay, submitAttempted]);
6594
+ return /* @__PURE__ */ jsx48(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx48(
4256
6595
  FormContainer,
4257
6596
  {
4258
6597
  displayContents,
@@ -4265,21 +6604,23 @@ function JsonSchemaFormRenderer(props) {
4265
6604
  disabled,
4266
6605
  hasChanges: hasChanges || allowPristineSave,
4267
6606
  isValid,
6607
+ isValidating,
4268
6608
  saveWhenInvalid,
4269
6609
  saveLabel,
4270
6610
  cancelLabel,
4271
6611
  showCancel,
4272
6612
  leadingActions,
4273
6613
  hiddenErrorCount,
6614
+ formLevelErrors,
4274
6615
  children: layoutFields
4275
6616
  }
4276
6617
  ) });
4277
6618
  }
4278
6619
 
4279
6620
  // src/hooks/useResolvedSchema.ts
4280
- import { useMemo as useMemo9 } from "react";
6621
+ import { useMemo as useMemo13 } from "react";
4281
6622
  import {
4282
- getSchemaProperties as getSchemaProperties2,
6623
+ getSchemaProperties as getSchemaProperties5,
4283
6624
  resolveSchema as resolveSchema2
4284
6625
  } from "@petrarca/sonnet-core/schema";
4285
6626
  function useResolvedSchema({
@@ -4287,12 +6628,12 @@ function useResolvedSchema({
4287
6628
  formData,
4288
6629
  uiSchema
4289
6630
  }) {
4290
- const resolvedSchema = useMemo9(
6631
+ const resolvedSchema = useMemo13(
4291
6632
  () => resolveSchema2(schema, formData),
4292
6633
  [schema, formData]
4293
6634
  );
4294
- const orderedProperties = useMemo9(() => {
4295
- const properties = getSchemaProperties2(resolvedSchema);
6635
+ const orderedProperties = useMemo13(() => {
6636
+ const properties = getSchemaProperties5(resolvedSchema);
4296
6637
  const rawOrder = uiSchema["x-ui-order"] ?? resolvedSchema["x-ui-order"];
4297
6638
  const orderArray = Array.isArray(rawOrder) ? rawOrder : void 0;
4298
6639
  return orderProperties(properties, orderArray);
@@ -4300,8 +6641,13 @@ function useResolvedSchema({
4300
6641
  return { resolvedSchema, orderedProperties };
4301
6642
  }
4302
6643
  export {
6644
+ AbsentReasonsWidget,
4303
6645
  ArrayWidget,
6646
+ CheckboxGroupWidget,
4304
6647
  CheckboxWidget,
6648
+ ChoiceWithTextWidget,
6649
+ CodeableConceptWidget,
6650
+ CodedValueWidget,
4305
6651
  DEFAULT_TEMPLATES,
4306
6652
  DEFAULT_WIDGETS,
4307
6653
  DEFAULT_WIDGET_MAP,
@@ -4315,6 +6661,7 @@ export {
4315
6661
  FormFieldRenderer,
4316
6662
  FormFieldWrapper,
4317
6663
  FormInput,
6664
+ FormKeyValueInput,
4318
6665
  FormMultiSelect,
4319
6666
  FormNumberInput,
4320
6667
  FormQuantityInput,
@@ -4324,10 +6671,12 @@ export {
4324
6671
  GroupErrorBadge,
4325
6672
  JsonEditorWidget,
4326
6673
  JsonSchemaFormRenderer,
6674
+ KeyValueWidget,
4327
6675
  NumberWidget,
4328
6676
  ObjectWidget,
4329
6677
  OneOfWidget,
4330
6678
  QuantityWidget,
6679
+ RadioWidget,
4331
6680
  SectionsGroupTemplate,
4332
6681
  SelectWidget,
4333
6682
  StepperGroupTemplate,