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