@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
package/dist/index.js CHANGED
@@ -60,6 +60,211 @@ function isFormEvent(value) {
60
60
  return typeof value.field === "string" && (value.value === void 0 || isJson(value.value)) && (value.source === void 0 || value.source === "prefill");
61
61
  }
62
62
 
63
+ // src/structured.ts
64
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
65
+ function intProp(field2, name, fallback, floor = 1) {
66
+ const v = field2.props?.[name];
67
+ return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : fallback;
68
+ }
69
+ function optionalIntProp(field2, name, floor = 0) {
70
+ const v = field2.props?.[name];
71
+ return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : void 0;
72
+ }
73
+ var blank = (v) => typeof v !== "string" || v.trim() === "";
74
+ var blankText = (v) => typeof v === "string" && v.trim() === "";
75
+ var emptyList = (value, isBlank) => value === void 0 || value === null || Array.isArray(value) && value.every(isBlank);
76
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
77
+ function listLimits(field2) {
78
+ return { min: optionalIntProp(field2, "min") ?? 0, max: intProp(field2, "max", 10), itemMaxLength: intProp(field2, "itemMaxLength", 500) };
79
+ }
80
+ var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
81
+ var MAX_TABLE_COLUMNS = 6;
82
+ var MAX_TABLE_CELL_LENGTH = 2e3;
83
+ function tableColumns(field2) {
84
+ const raw = field2.props?.columns;
85
+ if (!Array.isArray(raw)) return [];
86
+ const seen = /* @__PURE__ */ new Set();
87
+ const out = [];
88
+ for (const c of raw) {
89
+ if (!isObject(c) || typeof c.key !== "string" || !c.key || seen.has(c.key)) continue;
90
+ seen.add(c.key);
91
+ const column = { key: c.key, label: typeof c.label === "string" ? c.label : c.key };
92
+ if (c.long === true) column.long = true;
93
+ out.push(column);
94
+ }
95
+ return out;
96
+ }
97
+ function tableLimits(field2) {
98
+ return { minRows: optionalIntProp(field2, "minRows") ?? 0, maxRows: intProp(field2, "maxRows", 3) };
99
+ }
100
+ var filledRows = (value) => Array.isArray(value) ? value.filter((r) => isObject(r) && Object.values(r).some((c) => !blank(c))) : [];
101
+ var MAX_TIMESTAMP_SECONDS = 86400;
102
+ function formatTimestamp(seconds) {
103
+ const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
104
+ const h = Math.floor(total / 3600);
105
+ const m = Math.floor(total % 3600 / 60);
106
+ const s = String(total % 60).padStart(2, "0");
107
+ return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
108
+ }
109
+ function parseTimestamp(text) {
110
+ const t = text.trim();
111
+ if (!t) return null;
112
+ const parts = t.split(":");
113
+ if (parts.length > 3) return null;
114
+ if (parts.length === 1) {
115
+ if (!/^\d+(\.\d+)?$/.test(t)) return null;
116
+ const n = Number(t);
117
+ return n <= MAX_TIMESTAMP_SECONDS ? n : null;
118
+ }
119
+ const last = parts.length - 1;
120
+ let total = 0;
121
+ for (let i = 0; i < parts.length; i++) {
122
+ const p = parts[i];
123
+ const ok = i === 0 ? /^\d+$/.test(p) : i === last ? /^\d{2}(\.\d+)?$/.test(p) : /^\d{2}$/.test(p);
124
+ if (!ok) return null;
125
+ const n = Number(p);
126
+ if (i > 0 && n >= 60) return null;
127
+ total = total * 60 + n;
128
+ }
129
+ return total <= MAX_TIMESTAMP_SECONDS ? total : null;
130
+ }
131
+ var isTimestamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_TIMESTAMP_SECONDS;
132
+ var notesWithText = (value) => Array.isArray(value) ? value.filter((n) => isObject(n) && !blank(n.text)) : [];
133
+ var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
134
+ var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
135
+ var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
136
+ var sourceText = (field2) => typeof field2.props?.text === "string" && field2.props.text ? field2.props.text : void 0;
137
+ var STRUCTURED_FIELD_TYPES = ["list", "table", "media-notes", "text-highlights"];
138
+ var structuredFieldTypes = [
139
+ {
140
+ type: "list",
141
+ label: "List",
142
+ category: "text",
143
+ value: { json: true },
144
+ // logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
145
+ operators: ["contains", "answered", "empty"],
146
+ a11y: { semantics: "group" },
147
+ isEmpty: (value) => emptyList(value, blankText),
148
+ validate: (value, field2) => {
149
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return "Enter a list of answers.";
150
+ const { min, max, itemMaxLength } = listLimits(field2);
151
+ if (value.some((v) => v.trim().length > itemMaxLength)) return `Keep each item under ${itemMaxLength} characters.`;
152
+ const count = listItems(value).length;
153
+ if (count < min) return `Add at least ${plural(min, "item")}.`;
154
+ if (count > max) return `Add at most ${plural(max, "item")}.`;
155
+ return null;
156
+ },
157
+ normalize: (value) => Array.isArray(value) ? listItems(value).map((v) => v.trim()) : value,
158
+ summarize: (value) => listItems(value).map((v) => v.trim()).join("; "),
159
+ samples: (field2) => {
160
+ const { min, max } = listLimits(field2);
161
+ return [Array.from({ length: Math.min(Math.max(1, min), max) }, (_, i) => `Item ${i + 1}`)];
162
+ }
163
+ },
164
+ {
165
+ type: "table",
166
+ label: "Table",
167
+ category: "advanced",
168
+ value: { json: true },
169
+ operators: ["answered", "empty"],
170
+ a11y: { semantics: "group" },
171
+ isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
172
+ validate: (value, field2) => {
173
+ const columns = tableColumns(field2);
174
+ if (!columns.length) return "This question has no columns.";
175
+ if (!Array.isArray(value) || !value.every(isObject)) return "Fill in the rows.";
176
+ const keys = new Set(columns.map((c) => c.key));
177
+ for (const row of value) {
178
+ for (const [key, cell] of Object.entries(row)) {
179
+ if (!keys.has(key)) return "An answer names a column this question doesn't have.";
180
+ if (typeof cell !== "string") return "Fill in the rows with text.";
181
+ if (cell.trim().length > MAX_TABLE_CELL_LENGTH) return `Keep each answer under ${MAX_TABLE_CELL_LENGTH} characters.`;
182
+ }
183
+ }
184
+ const { minRows, maxRows } = tableLimits(field2);
185
+ const count = filledRows(value).length;
186
+ if (count < minRows) return `Fill in at least ${plural(minRows, "row")}.`;
187
+ if (count > maxRows) return `Fill in at most ${plural(maxRows, "row")}.`;
188
+ return null;
189
+ },
190
+ normalize: (value) => Array.isArray(value) ? filledRows(value).map((row) => {
191
+ const out = {};
192
+ for (const [key, cell] of Object.entries(row)) if (typeof cell === "string") out[key] = cell.trim();
193
+ return out;
194
+ }) : value,
195
+ summarize: (value, field2) => {
196
+ const columns = tableColumns(field2);
197
+ return filledRows(value).map((row) => columns.filter((c) => !blank(row[c.key])).map((c) => `${c.label}: ${row[c.key].trim()}`).join(" \xB7 ")).join(" / ");
198
+ },
199
+ samples: (field2) => {
200
+ const columns = tableColumns(field2);
201
+ if (!columns.length) return [];
202
+ const { minRows, maxRows } = tableLimits(field2);
203
+ const row = Object.fromEntries(columns.map((c) => [c.key, "sample"]));
204
+ return [Array.from({ length: Math.min(Math.max(1, minRows), maxRows) }, () => ({ ...row }))];
205
+ }
206
+ },
207
+ {
208
+ type: "media-notes",
209
+ label: "Notes at moments",
210
+ category: "media",
211
+ value: { json: true },
212
+ operators: ["answered", "empty"],
213
+ a11y: { semantics: "group" },
214
+ isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
215
+ validate: (value, field2) => {
216
+ if (!Array.isArray(value)) return "Add notes.";
217
+ const textMaxLength = intProp(field2, "textMaxLength", 1e3);
218
+ for (const note of value) {
219
+ if (!isObject(note) || typeof note.text !== "string") return "Add notes.";
220
+ if (!isTimestamp(note.at)) return "Each note needs a time.";
221
+ if (note.text.trim().length > textMaxLength) return `Keep each note under ${textMaxLength} characters.`;
222
+ }
223
+ const maxNotes = intProp(field2, "maxNotes", 20);
224
+ return notesWithText(value).length > maxNotes ? `Add at most ${plural(maxNotes, "note")}.` : null;
225
+ },
226
+ normalize: (value) => Array.isArray(value) ? notesWithText(value).map((n) => ({ at: n.at, text: n.text.trim() })).sort((a, b) => a.at - b.at) : value,
227
+ 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("; "),
228
+ samples: () => [[{ at: 0, text: "sample" }]]
229
+ },
230
+ {
231
+ type: "text-highlights",
232
+ label: "Highlights",
233
+ category: "media",
234
+ value: { json: true },
235
+ operators: ["answered", "empty"],
236
+ a11y: { semantics: "group" },
237
+ isEmpty: (value) => emptyList(value, () => false),
238
+ validate: (value, field2) => {
239
+ if (!Array.isArray(value)) return "Highlight a passage.";
240
+ const maxHighlights = intProp(field2, "maxHighlights", 20);
241
+ if (value.length > maxHighlights) return `Add at most ${plural(maxHighlights, "highlight")}.`;
242
+ const text = sourceText(field2);
243
+ for (const h of value) {
244
+ if (!isObject(h) || !Number.isInteger(h.start) || !Number.isInteger(h.end) || typeof h.quote !== "string" || typeof h.note !== "string") {
245
+ return "Highlight a passage of the text.";
246
+ }
247
+ const start = h.start;
248
+ const end = h.end;
249
+ if (start < 0 || end <= start || h.quote.length !== end - start) return "Highlight a passage of the text.";
250
+ if (end - start > MAX_HIGHLIGHT_QUOTE_LENGTH) return `Highlight under ${MAX_HIGHLIGHT_QUOTE_LENGTH} characters at a time.`;
251
+ if (text !== void 0 && (end > text.length || text.slice(start, end) !== h.quote)) return "A highlight doesn't match the text.";
252
+ if (h.note.trim() === "") return HIGHLIGHT_NOTE_MESSAGE;
253
+ if (h.note.trim().length > MAX_HIGHLIGHT_NOTE_LENGTH) return `Keep each note under ${MAX_HIGHLIGHT_NOTE_LENGTH} characters.`;
254
+ }
255
+ return null;
256
+ },
257
+ 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,
258
+ summarize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => `"${String(h.quote)}" \u2014 ${String(h.note).trim()}`).join("; ") : "",
259
+ samples: (field2) => {
260
+ const text = sourceText(field2);
261
+ if (text === void 0) return [[{ start: 0, end: 6, quote: "sample", note: "sample" }]];
262
+ const end = Math.min(text.length, 20);
263
+ return [[{ start: 0, end, quote: text.slice(0, end), note: "sample" }]];
264
+ }
265
+ }
266
+ ];
267
+
63
268
  // src/fields.ts
64
269
  var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
65
270
  var globalScope = globalThis;
@@ -247,7 +452,7 @@ var builtins = [
247
452
  summarize: (value) => value ? "Yes" : "No"
248
453
  }
249
454
  ];
250
- for (const manifest of builtins) registerFieldType(manifest);
455
+ for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
251
456
 
252
457
  // src/logic.ts
253
458
  function own(record, key) {
@@ -407,7 +612,7 @@ function shown(v) {
407
612
  text ??= typeof v === "bigint" ? `${v}n` : typeof v;
408
613
  return text.length > 60 ? `${text.slice(0, 57)}...` : text;
409
614
  }
410
- var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
615
+ var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
411
616
  var isInt = (v) => typeof v === "number" && Number.isInteger(v);
412
617
  var LITERAL_TYPE = {
413
618
  "short-text": "string",
@@ -418,7 +623,8 @@ var LITERAL_TYPE = {
418
623
  nps: "number",
419
624
  "single-choice": "string",
420
625
  "multi-choice": "string",
421
- "yes-no": "boolean"
626
+ "yes-no": "boolean",
627
+ list: "string"
422
628
  };
423
629
  var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
424
630
  var OPERATORS = /* @__PURE__ */ new Set([
@@ -460,7 +666,48 @@ function parseForm(input) {
460
666
  }
461
667
  return true;
462
668
  };
463
- if (!isObject(input)) {
669
+ const optionalInt = (v, path, floor) => {
670
+ if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
671
+ };
672
+ const ordered = (low, high, path, what) => {
673
+ if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
674
+ };
675
+ const structuredProps = (type, props2, path) => {
676
+ if (type === "list") {
677
+ optionalInt(props2.min, `${path}.min`, 0);
678
+ optionalInt(props2.max, `${path}.max`, 1);
679
+ optionalInt(props2.itemMaxLength, `${path}.itemMaxLength`, 1);
680
+ optionalText(props2.placeholder, `${path}.placeholder`);
681
+ ordered(props2.min, props2.max ?? 10, `${path}.min`, "most items");
682
+ } else if (type === "table") {
683
+ const columns = props2.columns;
684
+ if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
685
+ issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
686
+ } else {
687
+ const seen = /* @__PURE__ */ new Set();
688
+ columns.forEach((c, j) => {
689
+ const at = `${path}.columns[${j}]`;
690
+ if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
691
+ if (name(c.key, `${at}.key`, "A column key")) {
692
+ if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
693
+ seen.add(c.key);
694
+ }
695
+ text(c.label, `${at}.label`);
696
+ optionalBoolean(c.long, `${at}.long`);
697
+ });
698
+ }
699
+ optionalInt(props2.minRows, `${path}.minRows`, 0);
700
+ optionalInt(props2.maxRows, `${path}.maxRows`, 1);
701
+ ordered(props2.minRows, props2.maxRows ?? 3, `${path}.minRows`, "most rows");
702
+ } else if (type === "media-notes") {
703
+ optionalInt(props2.maxNotes, `${path}.maxNotes`, 1);
704
+ optionalInt(props2.textMaxLength, `${path}.textMaxLength`, 1);
705
+ } else if (type === "text-highlights") {
706
+ optionalText(props2.text, `${path}.text`);
707
+ optionalInt(props2.maxHighlights, `${path}.maxHighlights`, 1);
708
+ }
709
+ };
710
+ if (!isObject2(input)) {
464
711
  issue("", "A form definition must be an object.");
465
712
  return fail();
466
713
  }
@@ -469,19 +716,19 @@ function parseForm(input) {
469
716
  text(input.title, "title");
470
717
  optionalText(input.description, "description");
471
718
  if (input.layout !== void 0) {
472
- if (!isObject(input.layout)) issue("layout", "Must be an object.");
719
+ if (!isObject2(input.layout)) issue("layout", "Must be an object.");
473
720
  else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
474
721
  issue("layout.mode", "Must be focus or classic.");
475
722
  }
476
723
  }
477
724
  if (input.settings !== void 0) {
478
- if (!isObject(input.settings)) issue("settings", "Must be an object.");
725
+ if (!isObject2(input.settings)) issue("settings", "Must be an object.");
479
726
  else {
480
727
  optionalBoolean(input.settings.anonymous, "settings.anonymous");
481
728
  optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
482
729
  }
483
730
  }
484
- if (input.meta !== void 0 && (!isObject(input.meta) || !isJson(input.meta))) {
731
+ if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
485
732
  issue("meta", "Must be a JSON object.");
486
733
  }
487
734
  if (!Array.isArray(input.blocks)) {
@@ -496,7 +743,7 @@ function parseForm(input) {
496
743
  const choices = /* @__PURE__ */ new Map();
497
744
  input.blocks.forEach((block, i) => {
498
745
  const path = `blocks[${i}]`;
499
- if (!isObject(block) || typeof block.id !== "string" || !block.id) {
746
+ if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
500
747
  issue(path, "Every block needs a string id.");
501
748
  return;
502
749
  }
@@ -517,23 +764,23 @@ function parseForm(input) {
517
764
  issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
518
765
  }
519
766
  if (block.props !== void 0) {
520
- if (!isObject(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
767
+ if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
521
768
  else if (block.props.options !== void 0) {
522
769
  const options = block.props.options;
523
770
  if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
524
771
  else {
525
772
  options.forEach((o, j) => {
526
773
  const at = `${path}.props.options[${j}]`;
527
- if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
774
+ if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
528
775
  else optionalText(o.label, `${at}.label`);
529
776
  });
530
777
  choices.set(
531
778
  block.id,
532
- new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
779
+ new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
533
780
  );
534
781
  }
535
782
  }
536
- const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
783
+ const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
537
784
  if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
538
785
  issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
539
786
  }
@@ -541,6 +788,9 @@ function parseForm(input) {
541
788
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
542
789
  }
543
790
  }
791
+ if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
792
+ structuredProps(block.type, block.props ?? {}, `${path}.props`);
793
+ }
544
794
  if (validKey) {
545
795
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
546
796
  keys.set(block.key, block.id);
@@ -559,7 +809,7 @@ function parseForm(input) {
559
809
  const hiddenTypes = /* @__PURE__ */ new Map();
560
810
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
561
811
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
562
- if (!isObject(h) || typeof h.name !== "string" || !h.name) {
812
+ if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
563
813
  issue(`hidden[${i}]`, "Each hidden input needs a name.");
564
814
  return;
565
815
  }
@@ -580,7 +830,7 @@ function parseForm(input) {
580
830
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
581
831
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
582
832
  const path = `endings[${i}]`;
583
- if (!isObject(e)) {
833
+ if (!isObject2(e)) {
584
834
  issue(path, "Each ending must be an object with an id and a title.");
585
835
  return;
586
836
  }
@@ -592,7 +842,7 @@ function parseForm(input) {
592
842
  });
593
843
  const checkOperand = (o, path, reads) => {
594
844
  if (o === void 0) return;
595
- if (!isObject(o)) return issue(path, "An operand must be an object.");
845
+ if (!isObject2(o)) return issue(path, "An operand must be an object.");
596
846
  if ("field" in o) {
597
847
  if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
598
848
  reads.push(o.field);
@@ -605,7 +855,7 @@ function parseForm(input) {
605
855
  }
606
856
  };
607
857
  const checkCondition = (c, path, reads, depth = 0) => {
608
- if (!isObject(c)) return issue(path, "A condition must be an object.");
858
+ if (!isObject2(c)) return issue(path, "A condition must be an object.");
609
859
  if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
610
860
  if ("all" in c || "any" in c) {
611
861
  const group = "all" in c ? "all" : "any";
@@ -614,7 +864,7 @@ function parseForm(input) {
614
864
  return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
615
865
  }
616
866
  if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
617
- if (typeof c.op !== "string" || !isObject(c.left)) {
867
+ if (typeof c.op !== "string" || !isObject2(c.left)) {
618
868
  return issue(path, "A comparison needs an `op` and a `left` operand.");
619
869
  }
620
870
  if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
@@ -630,7 +880,7 @@ function parseForm(input) {
630
880
  };
631
881
  const checkLiterals = (op, left, right, path) => {
632
882
  if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
633
- const literal = isObject(right) && "value" in right;
883
+ const literal = isObject2(right) && "value" in right;
634
884
  if (right !== void 0 && !literal) return;
635
885
  let what = "the left side";
636
886
  let type;
@@ -669,7 +919,7 @@ function parseForm(input) {
669
919
  if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
670
920
  (Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
671
921
  const path = `logic[${i}]`;
672
- if (!isObject(rule) || typeof rule.id !== "string" || !isObject(rule.when)) {
922
+ if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
673
923
  issue(path, "Each rule needs an id and a `when` condition.");
674
924
  return;
675
925
  }
@@ -688,7 +938,7 @@ function parseForm(input) {
688
938
  at = rule.from;
689
939
  inclusive = true;
690
940
  }
691
- if (isObject(rule.to)) {
941
+ if (isObject2(rule.to)) {
692
942
  if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
693
943
  issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
694
944
  }
@@ -720,7 +970,7 @@ function parseForm(input) {
720
970
  }
721
971
  });
722
972
  input.blocks.forEach((block, i) => {
723
- if (!isObject(block)) return;
973
+ if (!isObject2(block)) return;
724
974
  const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
725
975
  const texts = raw.filter((t) => typeof t === "string");
726
976
  for (const t of texts) {
@@ -740,7 +990,7 @@ function parseForm(input) {
740
990
  });
741
991
  const extra = [["description", input.description]];
742
992
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
743
- if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
993
+ if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
744
994
  });
745
995
  for (const [path, t] of extra) {
746
996
  if (typeof t !== "string") continue;
@@ -754,7 +1004,7 @@ function parseForm(input) {
754
1004
  }
755
1005
 
756
1006
  // src/define.ts
757
- function field(type, key, options, props) {
1007
+ function field(type, key, options, props2) {
758
1008
  const block = {
759
1009
  kind: "field",
760
1010
  id: options.id ?? key,
@@ -767,9 +1017,10 @@ function field(type, key, options, props) {
767
1017
  if (options.required) block.required = true;
768
1018
  if (options.sensitive) block.sensitive = true;
769
1019
  if (options.identifying) block.identifying = true;
770
- if (props && Object.keys(props).length) block.props = props;
1020
+ if (props2 && Object.keys(props2).length) block.props = props2;
771
1021
  return block;
772
1022
  }
1023
+ var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
773
1024
  var choiceProps = (options) => ({
774
1025
  options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
775
1026
  });
@@ -789,6 +1040,41 @@ var f = {
789
1040
  singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
790
1041
  multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
791
1042
  yesNo: (key, o) => field("yes-no", key, o),
1043
+ /** Several short answers, one per line: goals, action items, three things that went well. */
1044
+ list: (key, o) => field(
1045
+ "list",
1046
+ key,
1047
+ o,
1048
+ props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
1049
+ ),
1050
+ /**
1051
+ * A small table each answer fills row by row, one answer per column: an
1052
+ * action plan (What / Who / By when), pros and cons.
1053
+ */
1054
+ table: (key, o) => field("table", key, o, {
1055
+ columns: o.columns.map((c) => c.long ? { key: c.key, label: c.label, long: true } : { key: c.key, label: c.label }),
1056
+ ...props({ minRows: o.minRows, maxRows: o.maxRows })
1057
+ }),
1058
+ /**
1059
+ * Notes pinned to times in a video or audio (lecture notes, a recorded call
1060
+ * review): `at` is seconds from its start.
1061
+ */
1062
+ mediaNotes: (key, o) => field(
1063
+ "media-notes",
1064
+ key,
1065
+ o,
1066
+ props({ maxNotes: o.maxNotes, textMaxLength: o.textMaxLength })
1067
+ ),
1068
+ /**
1069
+ * Highlight passages of a text and note on each (annotating an article, a
1070
+ * policy or a contract): `text` here, or supplied by the host app.
1071
+ */
1072
+ textHighlights: (key, o) => field(
1073
+ "text-highlights",
1074
+ key,
1075
+ o,
1076
+ props({ text: o.text, maxHighlights: o.maxHighlights })
1077
+ ),
792
1078
  /** Any registered field type, including plugins. */
793
1079
  custom: (type, key, o) => field(type, key, o, o.props),
794
1080
  heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
@@ -1259,6 +1545,6 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
1259
1545
  return `${left} ${op} ${right}`;
1260
1546
  }
1261
1547
 
1262
- export { MAX_HIDDEN_STRING_LENGTH, REQUIRED_MESSAGE, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, defineForm, describeCondition, evaluate, explain, f, fingerprint, getFieldType, isEmptyValue, isFormEvent, isJson, isSensitive, listFieldTypes, normalizeValue, parseForm, recall, registerFieldType, replay, ruleVisible, steps, summarizeValue, validateField, when };
1548
+ export { HIGHLIGHT_NOTE_MESSAGE, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, REQUIRED_MESSAGE, STRUCTURED_FIELD_TYPES, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, defineForm, describeCondition, evaluate, explain, f, fingerprint, formatTimestamp, getFieldType, isEmptyValue, isFormEvent, isJson, isSensitive, listFieldTypes, listLimits, normalizeValue, parseForm, parseTimestamp, recall, registerFieldType, replay, ruleVisible, steps, summarizeValue, tableColumns, tableLimits, validateField, when };
1263
1549
  //# sourceMappingURL=index.js.map
1264
1550
  //# sourceMappingURL=index.js.map