@marktiderman/genesis-forms 0.2.0 → 0.3.1

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.
Files changed (42) hide show
  1. package/README.md +113 -13
  2. package/conformance/{after-class-back-after-a-jump.scenario.json → event-feedback-back-after-a-jump.scenario.json} +21 -21
  3. package/conformance/{after-class-contact-yes-completed.scenario.json → event-feedback-contact-yes-completed.scenario.json} +21 -21
  4. package/conformance/{after-class-contact-yes-requires-name-and-email.scenario.json → event-feedback-contact-yes-requires-name-and-email.scenario.json} +19 -19
  5. package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json} +24 -24
  6. package/conformance/{after-class-invalid-email-rejected.scenario.json → event-feedback-invalid-email-rejected.scenario.json} +19 -19
  7. package/conformance/{after-class-low-rating-skips-went-well.scenario.json → event-feedback-low-rating-skips-went-well.scenario.json} +21 -21
  8. package/conformance/{after-class-server-drops-unknown-and-hidden-answers.scenario.json → event-feedback-server-drops-unknown-and-hidden-answers.scenario.json} +22 -22
  9. package/conformance/{after-class-server-ignores-a-client-sent-coach.scenario.json → event-feedback-server-ignores-a-client-sent-owner.scenario.json} +24 -24
  10. package/conformance/{monthly-pulse-default-ending.scenario.json → monthly-check-in-default-ending.scenario.json} +6 -6
  11. package/conformance/{monthly-pulse-promoter-ending-via-jump.scenario.json → monthly-check-in-promoter-ending-via-jump.scenario.json} +6 -6
  12. package/dist/destinations.cjs +1235 -0
  13. package/dist/destinations.cjs.map +1 -0
  14. package/dist/destinations.d.cts +465 -0
  15. package/dist/destinations.d.ts +465 -0
  16. package/dist/destinations.js +1193 -0
  17. package/dist/destinations.js.map +1 -0
  18. package/dist/{engine-Coc-gQIB.d.ts → engine-CPLtKVew.d.cts} +2 -1
  19. package/dist/{engine-Bj8-mMVb.d.cts → engine-DOrPgFDw.d.ts} +2 -1
  20. package/dist/index.cjs +322 -24
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +102 -5
  23. package/dist/index.d.ts +102 -5
  24. package/dist/index.js +311 -25
  25. package/dist/index.js.map +1 -1
  26. package/dist/server.cjs +249 -22
  27. package/dist/server.cjs.map +1 -1
  28. package/dist/server.d.cts +3 -2
  29. package/dist/server.d.ts +3 -2
  30. package/dist/server.js +249 -22
  31. package/dist/server.js.map +1 -1
  32. package/dist/testing.cjs +253 -22
  33. package/dist/testing.cjs.map +1 -1
  34. package/dist/testing.d.cts +4 -3
  35. package/dist/testing.d.ts +4 -3
  36. package/dist/testing.js +253 -22
  37. package/dist/testing.js.map +1 -1
  38. package/dist/{validate-BbackAjB.d.cts → types-BBKBQ8sp.d.cts} +2 -29
  39. package/dist/{validate-BbackAjB.d.ts → types-BBKBQ8sp.d.ts} +2 -29
  40. package/dist/validate-BXXqi0fq.d.cts +30 -0
  41. package/dist/validate-BjNwF1UE.d.ts +30 -0
  42. package/package.json +14 -2
@@ -1,6 +1,7 @@
1
- import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-Bj8-mMVb.cjs';
2
- export { d as compactEvents } from './engine-Bj8-mMVb.cjs';
3
- import { J as Json, a as FormDefinition, f as FormEvent } from './validate-BbackAjB.cjs';
1
+ import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-CPLtKVew.cjs';
2
+ export { d as compactEvents } from './engine-CPLtKVew.cjs';
3
+ import { J as Json, F as FormDefinition, e as FormEvent } from './types-BBKBQ8sp.cjs';
4
+ import './validate-BXXqi0fq.cjs';
4
5
 
5
6
  interface CoverageOptions {
6
7
  /** Stop after this many explored states and report `truncated`. Default 5000. */
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-Coc-gQIB.js';
2
- export { d as compactEvents } from './engine-Coc-gQIB.js';
3
- import { J as Json, a as FormDefinition, f as FormEvent } from './validate-BbackAjB.js';
1
+ import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-DOrPgFDw.js';
2
+ export { d as compactEvents } from './engine-DOrPgFDw.js';
3
+ import { J as Json, F as FormDefinition, e as FormEvent } from './types-BBKBQ8sp.js';
4
+ import './validate-BjNwF1UE.js';
4
5
 
5
6
  interface CoverageOptions {
6
7
  /** Stop after this many explored states and report `truncated`. Default 5000. */
package/dist/testing.js CHANGED
@@ -1,3 +1,185 @@
1
+ // src/structured.ts
2
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3
+ function intProp(field, name, fallback, floor = 1) {
4
+ const v = field.props?.[name];
5
+ return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : fallback;
6
+ }
7
+ function optionalIntProp(field, name, floor = 0) {
8
+ const v = field.props?.[name];
9
+ return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : void 0;
10
+ }
11
+ var blank = (v) => typeof v !== "string" || v.trim() === "";
12
+ var blankText = (v) => typeof v === "string" && v.trim() === "";
13
+ var emptyList = (value, isBlank) => value === void 0 || value === null || Array.isArray(value) && value.every(isBlank);
14
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
15
+ function listLimits(field) {
16
+ return { min: optionalIntProp(field, "min") ?? 0, max: intProp(field, "max", 10), itemMaxLength: intProp(field, "itemMaxLength", 500) };
17
+ }
18
+ var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
19
+ var MAX_TABLE_COLUMNS = 6;
20
+ var MAX_TABLE_CELL_LENGTH = 2e3;
21
+ function tableColumns(field) {
22
+ const raw = field.props?.columns;
23
+ if (!Array.isArray(raw)) return [];
24
+ const seen = /* @__PURE__ */ new Set();
25
+ const out = [];
26
+ for (const c of raw) {
27
+ if (!isObject(c) || typeof c.key !== "string" || !c.key || seen.has(c.key)) continue;
28
+ seen.add(c.key);
29
+ const column = { key: c.key, label: typeof c.label === "string" ? c.label : c.key };
30
+ if (c.long === true) column.long = true;
31
+ out.push(column);
32
+ }
33
+ return out;
34
+ }
35
+ function tableLimits(field) {
36
+ return { minRows: optionalIntProp(field, "minRows") ?? 0, maxRows: intProp(field, "maxRows", 3) };
37
+ }
38
+ var filledRows = (value) => Array.isArray(value) ? value.filter((r) => isObject(r) && Object.values(r).some((c) => !blank(c))) : [];
39
+ var MAX_TIMESTAMP_SECONDS = 86400;
40
+ function formatTimestamp(seconds) {
41
+ const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
42
+ const h = Math.floor(total / 3600);
43
+ const m = Math.floor(total % 3600 / 60);
44
+ const s = String(total % 60).padStart(2, "0");
45
+ return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
46
+ }
47
+ var isTimestamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_TIMESTAMP_SECONDS;
48
+ var notesWithText = (value) => Array.isArray(value) ? value.filter((n) => isObject(n) && !blank(n.text)) : [];
49
+ var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
50
+ var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
51
+ var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
52
+ var sourceText = (field) => typeof field.props?.text === "string" && field.props.text ? field.props.text : void 0;
53
+ var structuredFieldTypes = [
54
+ {
55
+ type: "list",
56
+ label: "List",
57
+ category: "text",
58
+ value: { json: true },
59
+ // logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
60
+ operators: ["contains", "answered", "empty"],
61
+ a11y: { semantics: "group" },
62
+ isEmpty: (value) => emptyList(value, blankText),
63
+ validate: (value, field) => {
64
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return "Enter a list of answers.";
65
+ const { min, max, itemMaxLength } = listLimits(field);
66
+ if (value.some((v) => v.trim().length > itemMaxLength)) return `Keep each item under ${itemMaxLength} characters.`;
67
+ const count = listItems(value).length;
68
+ if (count < min) return `Add at least ${plural(min, "item")}.`;
69
+ if (count > max) return `Add at most ${plural(max, "item")}.`;
70
+ return null;
71
+ },
72
+ normalize: (value) => Array.isArray(value) ? listItems(value).map((v) => v.trim()) : value,
73
+ summarize: (value) => listItems(value).map((v) => v.trim()).join("; "),
74
+ samples: (field) => {
75
+ const { min, max } = listLimits(field);
76
+ return [Array.from({ length: Math.min(Math.max(1, min), max) }, (_, i) => `Item ${i + 1}`)];
77
+ }
78
+ },
79
+ {
80
+ type: "table",
81
+ label: "Table",
82
+ category: "advanced",
83
+ value: { json: true },
84
+ operators: ["answered", "empty"],
85
+ a11y: { semantics: "group" },
86
+ isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
87
+ validate: (value, field) => {
88
+ const columns = tableColumns(field);
89
+ if (!columns.length) return "This question has no columns.";
90
+ if (!Array.isArray(value) || !value.every(isObject)) return "Fill in the rows.";
91
+ const keys = new Set(columns.map((c) => c.key));
92
+ for (const row of value) {
93
+ for (const [key, cell] of Object.entries(row)) {
94
+ if (!keys.has(key)) return "An answer names a column this question doesn't have.";
95
+ if (typeof cell !== "string") return "Fill in the rows with text.";
96
+ if (cell.trim().length > MAX_TABLE_CELL_LENGTH) return `Keep each answer under ${MAX_TABLE_CELL_LENGTH} characters.`;
97
+ }
98
+ }
99
+ const { minRows, maxRows } = tableLimits(field);
100
+ const count = filledRows(value).length;
101
+ if (count < minRows) return `Fill in at least ${plural(minRows, "row")}.`;
102
+ if (count > maxRows) return `Fill in at most ${plural(maxRows, "row")}.`;
103
+ return null;
104
+ },
105
+ normalize: (value) => Array.isArray(value) ? filledRows(value).map((row) => {
106
+ const out = {};
107
+ for (const [key, cell] of Object.entries(row)) if (typeof cell === "string") out[key] = cell.trim();
108
+ return out;
109
+ }) : value,
110
+ summarize: (value, field) => {
111
+ const columns = tableColumns(field);
112
+ return filledRows(value).map((row) => columns.filter((c) => !blank(row[c.key])).map((c) => `${c.label}: ${row[c.key].trim()}`).join(" \xB7 ")).join(" / ");
113
+ },
114
+ samples: (field) => {
115
+ const columns = tableColumns(field);
116
+ if (!columns.length) return [];
117
+ const { minRows, maxRows } = tableLimits(field);
118
+ const row = Object.fromEntries(columns.map((c) => [c.key, "sample"]));
119
+ return [Array.from({ length: Math.min(Math.max(1, minRows), maxRows) }, () => ({ ...row }))];
120
+ }
121
+ },
122
+ {
123
+ type: "media-notes",
124
+ label: "Notes at moments",
125
+ category: "media",
126
+ value: { json: true },
127
+ operators: ["answered", "empty"],
128
+ a11y: { semantics: "group" },
129
+ isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
130
+ validate: (value, field) => {
131
+ if (!Array.isArray(value)) return "Add notes.";
132
+ const textMaxLength = intProp(field, "textMaxLength", 1e3);
133
+ for (const note of value) {
134
+ if (!isObject(note) || typeof note.text !== "string") return "Add notes.";
135
+ if (!isTimestamp(note.at)) return "Each note needs a time.";
136
+ if (note.text.trim().length > textMaxLength) return `Keep each note under ${textMaxLength} characters.`;
137
+ }
138
+ const maxNotes = intProp(field, "maxNotes", 20);
139
+ return notesWithText(value).length > maxNotes ? `Add at most ${plural(maxNotes, "note")}.` : null;
140
+ },
141
+ normalize: (value) => Array.isArray(value) ? notesWithText(value).map((n) => ({ at: n.at, text: n.text.trim() })).sort((a, b) => a.at - b.at) : value,
142
+ summarize: (value) => notesWithText(value).map((n) => ({ at: typeof n.at === "number" ? n.at : 0, text: n.text.trim() })).sort((a, b) => a.at - b.at).map((n) => `${formatTimestamp(n.at)} ${n.text}`).join("; "),
143
+ samples: () => [[{ at: 0, text: "sample" }]]
144
+ },
145
+ {
146
+ type: "text-highlights",
147
+ label: "Highlights",
148
+ category: "media",
149
+ value: { json: true },
150
+ operators: ["answered", "empty"],
151
+ a11y: { semantics: "group" },
152
+ isEmpty: (value) => emptyList(value, () => false),
153
+ validate: (value, field) => {
154
+ if (!Array.isArray(value)) return "Highlight a passage.";
155
+ const maxHighlights = intProp(field, "maxHighlights", 20);
156
+ if (value.length > maxHighlights) return `Add at most ${plural(maxHighlights, "highlight")}.`;
157
+ const text = sourceText(field);
158
+ for (const h of value) {
159
+ if (!isObject(h) || !Number.isInteger(h.start) || !Number.isInteger(h.end) || typeof h.quote !== "string" || typeof h.note !== "string") {
160
+ return "Highlight a passage of the text.";
161
+ }
162
+ const start = h.start;
163
+ const end = h.end;
164
+ if (start < 0 || end <= start || h.quote.length !== end - start) return "Highlight a passage of the text.";
165
+ if (end - start > MAX_HIGHLIGHT_QUOTE_LENGTH) return `Highlight under ${MAX_HIGHLIGHT_QUOTE_LENGTH} characters at a time.`;
166
+ if (text !== void 0 && (end > text.length || text.slice(start, end) !== h.quote)) return "A highlight doesn't match the text.";
167
+ if (h.note.trim() === "") return HIGHLIGHT_NOTE_MESSAGE;
168
+ if (h.note.trim().length > MAX_HIGHLIGHT_NOTE_LENGTH) return `Keep each note under ${MAX_HIGHLIGHT_NOTE_LENGTH} characters.`;
169
+ }
170
+ return null;
171
+ },
172
+ normalize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => ({ start: h.start, end: h.end, quote: h.quote, note: String(h.note).trim() })).sort((a, b) => a.start - b.start || a.end - b.end) : value,
173
+ summarize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => `"${String(h.quote)}" \u2014 ${String(h.note).trim()}`).join("; ") : "",
174
+ samples: (field) => {
175
+ const text = sourceText(field);
176
+ if (text === void 0) return [[{ start: 0, end: 6, quote: "sample", note: "sample" }]];
177
+ const end = Math.min(text.length, 20);
178
+ return [[{ start: 0, end, quote: text.slice(0, end), note: "sample" }]];
179
+ }
180
+ }
181
+ ];
182
+
1
183
  // src/fields.ts
2
184
  var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
3
185
  var globalScope = globalThis;
@@ -179,7 +361,7 @@ var builtins = [
179
361
  summarize: (value) => value ? "Yes" : "No"
180
362
  }
181
363
  ];
182
- for (const manifest of builtins) registerFieldType(manifest);
364
+ for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
183
365
 
184
366
  // src/events.ts
185
367
  function isPlainObject(value) {
@@ -668,6 +850,10 @@ function domainOf(field, extra, maxOptions) {
668
850
  extra = extra.map((v) => typeof v === "string" ? [v] : v);
669
851
  break;
670
852
  }
853
+ case "list":
854
+ base = getFieldType(field.type)?.samples?.(field) ?? [];
855
+ extra = extra.map((v) => typeof v === "string" ? [v] : v);
856
+ break;
671
857
  case "email":
672
858
  base = ["ada@example.com"];
673
859
  break;
@@ -781,7 +967,7 @@ function shown(v) {
781
967
  text ??= typeof v === "bigint" ? `${v}n` : typeof v;
782
968
  return text.length > 60 ? `${text.slice(0, 57)}...` : text;
783
969
  }
784
- var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
970
+ var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
785
971
  var isInt = (v) => typeof v === "number" && Number.isInteger(v);
786
972
  var LITERAL_TYPE = {
787
973
  "short-text": "string",
@@ -792,7 +978,8 @@ var LITERAL_TYPE = {
792
978
  nps: "number",
793
979
  "single-choice": "string",
794
980
  "multi-choice": "string",
795
- "yes-no": "boolean"
981
+ "yes-no": "boolean",
982
+ list: "string"
796
983
  };
797
984
  var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
798
985
  var OPERATORS = /* @__PURE__ */ new Set([
@@ -834,7 +1021,48 @@ function parseForm(input) {
834
1021
  }
835
1022
  return true;
836
1023
  };
837
- if (!isObject(input)) {
1024
+ const optionalInt = (v, path, floor) => {
1025
+ if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
1026
+ };
1027
+ const ordered = (low, high, path, what) => {
1028
+ if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
1029
+ };
1030
+ const structuredProps = (type, props, path) => {
1031
+ if (type === "list") {
1032
+ optionalInt(props.min, `${path}.min`, 0);
1033
+ optionalInt(props.max, `${path}.max`, 1);
1034
+ optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
1035
+ optionalText(props.placeholder, `${path}.placeholder`);
1036
+ ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
1037
+ } else if (type === "table") {
1038
+ const columns = props.columns;
1039
+ if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
1040
+ issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
1041
+ } else {
1042
+ const seen = /* @__PURE__ */ new Set();
1043
+ columns.forEach((c, j) => {
1044
+ const at = `${path}.columns[${j}]`;
1045
+ if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
1046
+ if (name(c.key, `${at}.key`, "A column key")) {
1047
+ if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
1048
+ seen.add(c.key);
1049
+ }
1050
+ text(c.label, `${at}.label`);
1051
+ optionalBoolean(c.long, `${at}.long`);
1052
+ });
1053
+ }
1054
+ optionalInt(props.minRows, `${path}.minRows`, 0);
1055
+ optionalInt(props.maxRows, `${path}.maxRows`, 1);
1056
+ ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
1057
+ } else if (type === "media-notes") {
1058
+ optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
1059
+ optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
1060
+ } else if (type === "text-highlights") {
1061
+ optionalText(props.text, `${path}.text`);
1062
+ optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
1063
+ }
1064
+ };
1065
+ if (!isObject2(input)) {
838
1066
  issue("", "A form definition must be an object.");
839
1067
  return fail();
840
1068
  }
@@ -843,19 +1071,19 @@ function parseForm(input) {
843
1071
  text(input.title, "title");
844
1072
  optionalText(input.description, "description");
845
1073
  if (input.layout !== void 0) {
846
- if (!isObject(input.layout)) issue("layout", "Must be an object.");
1074
+ if (!isObject2(input.layout)) issue("layout", "Must be an object.");
847
1075
  else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
848
1076
  issue("layout.mode", "Must be focus or classic.");
849
1077
  }
850
1078
  }
851
1079
  if (input.settings !== void 0) {
852
- if (!isObject(input.settings)) issue("settings", "Must be an object.");
1080
+ if (!isObject2(input.settings)) issue("settings", "Must be an object.");
853
1081
  else {
854
1082
  optionalBoolean(input.settings.anonymous, "settings.anonymous");
855
1083
  optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
856
1084
  }
857
1085
  }
858
- if (input.meta !== void 0 && (!isObject(input.meta) || !isJson(input.meta))) {
1086
+ if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
859
1087
  issue("meta", "Must be a JSON object.");
860
1088
  }
861
1089
  if (!Array.isArray(input.blocks)) {
@@ -870,7 +1098,7 @@ function parseForm(input) {
870
1098
  const choices = /* @__PURE__ */ new Map();
871
1099
  input.blocks.forEach((block, i) => {
872
1100
  const path = `blocks[${i}]`;
873
- if (!isObject(block) || typeof block.id !== "string" || !block.id) {
1101
+ if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
874
1102
  issue(path, "Every block needs a string id.");
875
1103
  return;
876
1104
  }
@@ -891,23 +1119,23 @@ function parseForm(input) {
891
1119
  issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
892
1120
  }
893
1121
  if (block.props !== void 0) {
894
- if (!isObject(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
1122
+ if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
895
1123
  else if (block.props.options !== void 0) {
896
1124
  const options = block.props.options;
897
1125
  if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
898
1126
  else {
899
1127
  options.forEach((o, j) => {
900
1128
  const at = `${path}.props.options[${j}]`;
901
- if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
1129
+ if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
902
1130
  else optionalText(o.label, `${at}.label`);
903
1131
  });
904
1132
  choices.set(
905
1133
  block.id,
906
- new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
1134
+ new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
907
1135
  );
908
1136
  }
909
1137
  }
910
- const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
1138
+ const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
911
1139
  if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
912
1140
  issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
913
1141
  }
@@ -915,6 +1143,9 @@ function parseForm(input) {
915
1143
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
916
1144
  }
917
1145
  }
1146
+ if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
1147
+ structuredProps(block.type, block.props ?? {}, `${path}.props`);
1148
+ }
918
1149
  if (validKey) {
919
1150
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
920
1151
  keys.set(block.key, block.id);
@@ -933,7 +1164,7 @@ function parseForm(input) {
933
1164
  const hiddenTypes = /* @__PURE__ */ new Map();
934
1165
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
935
1166
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
936
- if (!isObject(h) || typeof h.name !== "string" || !h.name) {
1167
+ if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
937
1168
  issue(`hidden[${i}]`, "Each hidden input needs a name.");
938
1169
  return;
939
1170
  }
@@ -954,7 +1185,7 @@ function parseForm(input) {
954
1185
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
955
1186
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
956
1187
  const path = `endings[${i}]`;
957
- if (!isObject(e)) {
1188
+ if (!isObject2(e)) {
958
1189
  issue(path, "Each ending must be an object with an id and a title.");
959
1190
  return;
960
1191
  }
@@ -966,7 +1197,7 @@ function parseForm(input) {
966
1197
  });
967
1198
  const checkOperand = (o, path, reads) => {
968
1199
  if (o === void 0) return;
969
- if (!isObject(o)) return issue(path, "An operand must be an object.");
1200
+ if (!isObject2(o)) return issue(path, "An operand must be an object.");
970
1201
  if ("field" in o) {
971
1202
  if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
972
1203
  reads.push(o.field);
@@ -979,7 +1210,7 @@ function parseForm(input) {
979
1210
  }
980
1211
  };
981
1212
  const checkCondition = (c, path, reads, depth = 0) => {
982
- if (!isObject(c)) return issue(path, "A condition must be an object.");
1213
+ if (!isObject2(c)) return issue(path, "A condition must be an object.");
983
1214
  if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
984
1215
  if ("all" in c || "any" in c) {
985
1216
  const group = "all" in c ? "all" : "any";
@@ -988,7 +1219,7 @@ function parseForm(input) {
988
1219
  return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
989
1220
  }
990
1221
  if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
991
- if (typeof c.op !== "string" || !isObject(c.left)) {
1222
+ if (typeof c.op !== "string" || !isObject2(c.left)) {
992
1223
  return issue(path, "A comparison needs an `op` and a `left` operand.");
993
1224
  }
994
1225
  if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
@@ -1004,7 +1235,7 @@ function parseForm(input) {
1004
1235
  };
1005
1236
  const checkLiterals = (op, left, right, path) => {
1006
1237
  if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
1007
- const literal = isObject(right) && "value" in right;
1238
+ const literal = isObject2(right) && "value" in right;
1008
1239
  if (right !== void 0 && !literal) return;
1009
1240
  let what = "the left side";
1010
1241
  let type;
@@ -1043,7 +1274,7 @@ function parseForm(input) {
1043
1274
  if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
1044
1275
  (Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
1045
1276
  const path = `logic[${i}]`;
1046
- if (!isObject(rule) || typeof rule.id !== "string" || !isObject(rule.when)) {
1277
+ if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
1047
1278
  issue(path, "Each rule needs an id and a `when` condition.");
1048
1279
  return;
1049
1280
  }
@@ -1062,7 +1293,7 @@ function parseForm(input) {
1062
1293
  at = rule.from;
1063
1294
  inclusive = true;
1064
1295
  }
1065
- if (isObject(rule.to)) {
1296
+ if (isObject2(rule.to)) {
1066
1297
  if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
1067
1298
  issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
1068
1299
  }
@@ -1094,7 +1325,7 @@ function parseForm(input) {
1094
1325
  }
1095
1326
  });
1096
1327
  input.blocks.forEach((block, i) => {
1097
- if (!isObject(block)) return;
1328
+ if (!isObject2(block)) return;
1098
1329
  const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
1099
1330
  const texts = raw.filter((t) => typeof t === "string");
1100
1331
  for (const t of texts) {
@@ -1114,7 +1345,7 @@ function parseForm(input) {
1114
1345
  });
1115
1346
  const extra = [["description", input.description]];
1116
1347
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
1117
- if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1348
+ if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1118
1349
  });
1119
1350
  for (const [path, t] of extra) {
1120
1351
  if (typeof t !== "string") continue;