@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
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,29 +764,30 @@ 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
  }
540
787
  if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
541
788
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
542
789
  }
790
+ if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
543
791
  }
544
792
  if (validKey) {
545
793
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
@@ -559,7 +807,7 @@ function parseForm(input) {
559
807
  const hiddenTypes = /* @__PURE__ */ new Map();
560
808
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
561
809
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
562
- if (!isObject(h) || typeof h.name !== "string" || !h.name) {
810
+ if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
563
811
  issue(`hidden[${i}]`, "Each hidden input needs a name.");
564
812
  return;
565
813
  }
@@ -580,7 +828,7 @@ function parseForm(input) {
580
828
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
581
829
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
582
830
  const path = `endings[${i}]`;
583
- if (!isObject(e)) {
831
+ if (!isObject2(e)) {
584
832
  issue(path, "Each ending must be an object with an id and a title.");
585
833
  return;
586
834
  }
@@ -592,7 +840,7 @@ function parseForm(input) {
592
840
  });
593
841
  const checkOperand = (o, path, reads) => {
594
842
  if (o === void 0) return;
595
- if (!isObject(o)) return issue(path, "An operand must be an object.");
843
+ if (!isObject2(o)) return issue(path, "An operand must be an object.");
596
844
  if ("field" in o) {
597
845
  if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
598
846
  reads.push(o.field);
@@ -605,7 +853,7 @@ function parseForm(input) {
605
853
  }
606
854
  };
607
855
  const checkCondition = (c, path, reads, depth = 0) => {
608
- if (!isObject(c)) return issue(path, "A condition must be an object.");
856
+ if (!isObject2(c)) return issue(path, "A condition must be an object.");
609
857
  if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
610
858
  if ("all" in c || "any" in c) {
611
859
  const group = "all" in c ? "all" : "any";
@@ -614,7 +862,7 @@ function parseForm(input) {
614
862
  return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
615
863
  }
616
864
  if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
617
- if (typeof c.op !== "string" || !isObject(c.left)) {
865
+ if (typeof c.op !== "string" || !isObject2(c.left)) {
618
866
  return issue(path, "A comparison needs an `op` and a `left` operand.");
619
867
  }
620
868
  if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
@@ -630,7 +878,7 @@ function parseForm(input) {
630
878
  };
631
879
  const checkLiterals = (op, left, right, path) => {
632
880
  if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
633
- const literal = isObject(right) && "value" in right;
881
+ const literal = isObject2(right) && "value" in right;
634
882
  if (right !== void 0 && !literal) return;
635
883
  let what = "the left side";
636
884
  let type;
@@ -669,7 +917,7 @@ function parseForm(input) {
669
917
  if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
670
918
  (Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
671
919
  const path = `logic[${i}]`;
672
- if (!isObject(rule) || typeof rule.id !== "string" || !isObject(rule.when)) {
920
+ if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
673
921
  issue(path, "Each rule needs an id and a `when` condition.");
674
922
  return;
675
923
  }
@@ -688,7 +936,7 @@ function parseForm(input) {
688
936
  at = rule.from;
689
937
  inclusive = true;
690
938
  }
691
- if (isObject(rule.to)) {
939
+ if (isObject2(rule.to)) {
692
940
  if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
693
941
  issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
694
942
  }
@@ -720,7 +968,7 @@ function parseForm(input) {
720
968
  }
721
969
  });
722
970
  input.blocks.forEach((block, i) => {
723
- if (!isObject(block)) return;
971
+ if (!isObject2(block)) return;
724
972
  const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
725
973
  const texts = raw.filter((t) => typeof t === "string");
726
974
  for (const t of texts) {
@@ -740,7 +988,7 @@ function parseForm(input) {
740
988
  });
741
989
  const extra = [["description", input.description]];
742
990
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
743
- if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
991
+ if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
744
992
  });
745
993
  for (const [path, t] of extra) {
746
994
  if (typeof t !== "string") continue;
@@ -754,7 +1002,7 @@ function parseForm(input) {
754
1002
  }
755
1003
 
756
1004
  // src/define.ts
757
- function field(type, key, options, props) {
1005
+ function field(type, key, options, props2) {
758
1006
  const block = {
759
1007
  kind: "field",
760
1008
  id: options.id ?? key,
@@ -767,9 +1015,10 @@ function field(type, key, options, props) {
767
1015
  if (options.required) block.required = true;
768
1016
  if (options.sensitive) block.sensitive = true;
769
1017
  if (options.identifying) block.identifying = true;
770
- if (props && Object.keys(props).length) block.props = props;
1018
+ if (props2 && Object.keys(props2).length) block.props = props2;
771
1019
  return block;
772
1020
  }
1021
+ var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
773
1022
  var choiceProps = (options) => ({
774
1023
  options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
775
1024
  });
@@ -789,6 +1038,41 @@ var f = {
789
1038
  singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
790
1039
  multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
791
1040
  yesNo: (key, o) => field("yes-no", key, o),
1041
+ /** Several short answers, one per line: goals, action items, three things that went well. */
1042
+ list: (key, o) => field(
1043
+ "list",
1044
+ key,
1045
+ o,
1046
+ props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
1047
+ ),
1048
+ /**
1049
+ * A small table each answer fills row by row, one answer per column: an
1050
+ * action plan (What / Who / By when), pros and cons.
1051
+ */
1052
+ table: (key, o) => field("table", key, o, {
1053
+ columns: o.columns.map((c) => c.long ? { key: c.key, label: c.label, long: true } : { key: c.key, label: c.label }),
1054
+ ...props({ minRows: o.minRows, maxRows: o.maxRows })
1055
+ }),
1056
+ /**
1057
+ * Notes pinned to times in a video or audio (lecture notes, a recorded call
1058
+ * review): `at` is seconds from its start.
1059
+ */
1060
+ mediaNotes: (key, o) => field(
1061
+ "media-notes",
1062
+ key,
1063
+ o,
1064
+ props({ maxNotes: o.maxNotes, textMaxLength: o.textMaxLength })
1065
+ ),
1066
+ /**
1067
+ * Highlight passages of a text and note on each (annotating an article, a
1068
+ * policy or a contract): `text` here, or supplied by the host app.
1069
+ */
1070
+ textHighlights: (key, o) => field(
1071
+ "text-highlights",
1072
+ key,
1073
+ o,
1074
+ props({ text: o.text, maxHighlights: o.maxHighlights })
1075
+ ),
792
1076
  /** Any registered field type, including plugins. */
793
1077
  custom: (type, key, o) => field(type, key, o, o.props),
794
1078
  heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
@@ -1259,6 +1543,6 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
1259
1543
  return `${left} ${op} ${right}`;
1260
1544
  }
1261
1545
 
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 };
1546
+ 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
1547
  //# sourceMappingURL=index.js.map
1264
1548
  //# sourceMappingURL=index.js.map