@marktiderman/genesis-forms 0.2.0 → 0.3.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.
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 +1211 -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 +1169 -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 +320 -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 +309 -25
  25. package/dist/index.js.map +1 -1
  26. package/dist/server.cjs +247 -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 +247 -22
  31. package/dist/server.js.map +1 -1
  32. package/dist/testing.cjs +251 -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 +251 -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,29 +1119,30 @@ 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
  }
914
1142
  if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
915
1143
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
916
1144
  }
1145
+ if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
917
1146
  }
918
1147
  if (validKey) {
919
1148
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
@@ -933,7 +1162,7 @@ function parseForm(input) {
933
1162
  const hiddenTypes = /* @__PURE__ */ new Map();
934
1163
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
935
1164
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
936
- if (!isObject(h) || typeof h.name !== "string" || !h.name) {
1165
+ if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
937
1166
  issue(`hidden[${i}]`, "Each hidden input needs a name.");
938
1167
  return;
939
1168
  }
@@ -954,7 +1183,7 @@ function parseForm(input) {
954
1183
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
955
1184
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
956
1185
  const path = `endings[${i}]`;
957
- if (!isObject(e)) {
1186
+ if (!isObject2(e)) {
958
1187
  issue(path, "Each ending must be an object with an id and a title.");
959
1188
  return;
960
1189
  }
@@ -966,7 +1195,7 @@ function parseForm(input) {
966
1195
  });
967
1196
  const checkOperand = (o, path, reads) => {
968
1197
  if (o === void 0) return;
969
- if (!isObject(o)) return issue(path, "An operand must be an object.");
1198
+ if (!isObject2(o)) return issue(path, "An operand must be an object.");
970
1199
  if ("field" in o) {
971
1200
  if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
972
1201
  reads.push(o.field);
@@ -979,7 +1208,7 @@ function parseForm(input) {
979
1208
  }
980
1209
  };
981
1210
  const checkCondition = (c, path, reads, depth = 0) => {
982
- if (!isObject(c)) return issue(path, "A condition must be an object.");
1211
+ if (!isObject2(c)) return issue(path, "A condition must be an object.");
983
1212
  if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
984
1213
  if ("all" in c || "any" in c) {
985
1214
  const group = "all" in c ? "all" : "any";
@@ -988,7 +1217,7 @@ function parseForm(input) {
988
1217
  return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
989
1218
  }
990
1219
  if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
991
- if (typeof c.op !== "string" || !isObject(c.left)) {
1220
+ if (typeof c.op !== "string" || !isObject2(c.left)) {
992
1221
  return issue(path, "A comparison needs an `op` and a `left` operand.");
993
1222
  }
994
1223
  if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
@@ -1004,7 +1233,7 @@ function parseForm(input) {
1004
1233
  };
1005
1234
  const checkLiterals = (op, left, right, path) => {
1006
1235
  if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
1007
- const literal = isObject(right) && "value" in right;
1236
+ const literal = isObject2(right) && "value" in right;
1008
1237
  if (right !== void 0 && !literal) return;
1009
1238
  let what = "the left side";
1010
1239
  let type;
@@ -1043,7 +1272,7 @@ function parseForm(input) {
1043
1272
  if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
1044
1273
  (Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
1045
1274
  const path = `logic[${i}]`;
1046
- if (!isObject(rule) || typeof rule.id !== "string" || !isObject(rule.when)) {
1275
+ if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
1047
1276
  issue(path, "Each rule needs an id and a `when` condition.");
1048
1277
  return;
1049
1278
  }
@@ -1062,7 +1291,7 @@ function parseForm(input) {
1062
1291
  at = rule.from;
1063
1292
  inclusive = true;
1064
1293
  }
1065
- if (isObject(rule.to)) {
1294
+ if (isObject2(rule.to)) {
1066
1295
  if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
1067
1296
  issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
1068
1297
  }
@@ -1094,7 +1323,7 @@ function parseForm(input) {
1094
1323
  }
1095
1324
  });
1096
1325
  input.blocks.forEach((block, i) => {
1097
- if (!isObject(block)) return;
1326
+ if (!isObject2(block)) return;
1098
1327
  const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
1099
1328
  const texts = raw.filter((t) => typeof t === "string");
1100
1329
  for (const t of texts) {
@@ -1114,7 +1343,7 @@ function parseForm(input) {
1114
1343
  });
1115
1344
  const extra = [["description", input.description]];
1116
1345
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
1117
- if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1346
+ if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1118
1347
  });
1119
1348
  for (const [path, t] of extra) {
1120
1349
  if (typeof t !== "string") continue;