@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.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,23 +766,23 @@ 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
  }
@@ -543,6 +790,9 @@ function parseForm(input) {
543
790
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
544
791
  }
545
792
  }
793
+ if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
794
+ structuredProps(block.type, block.props ?? {}, `${path}.props`);
795
+ }
546
796
  if (validKey) {
547
797
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
548
798
  keys.set(block.key, block.id);
@@ -561,7 +811,7 @@ function parseForm(input) {
561
811
  const hiddenTypes = /* @__PURE__ */ new Map();
562
812
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
563
813
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
564
- if (!isObject(h) || typeof h.name !== "string" || !h.name) {
814
+ if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
565
815
  issue(`hidden[${i}]`, "Each hidden input needs a name.");
566
816
  return;
567
817
  }
@@ -582,7 +832,7 @@ function parseForm(input) {
582
832
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
583
833
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
584
834
  const path = `endings[${i}]`;
585
- if (!isObject(e)) {
835
+ if (!isObject2(e)) {
586
836
  issue(path, "Each ending must be an object with an id and a title.");
587
837
  return;
588
838
  }
@@ -594,7 +844,7 @@ function parseForm(input) {
594
844
  });
595
845
  const checkOperand = (o, path, reads) => {
596
846
  if (o === void 0) return;
597
- if (!isObject(o)) return issue(path, "An operand must be an object.");
847
+ if (!isObject2(o)) return issue(path, "An operand must be an object.");
598
848
  if ("field" in o) {
599
849
  if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
600
850
  reads.push(o.field);
@@ -607,7 +857,7 @@ function parseForm(input) {
607
857
  }
608
858
  };
609
859
  const checkCondition = (c, path, reads, depth = 0) => {
610
- if (!isObject(c)) return issue(path, "A condition must be an object.");
860
+ if (!isObject2(c)) return issue(path, "A condition must be an object.");
611
861
  if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
612
862
  if ("all" in c || "any" in c) {
613
863
  const group = "all" in c ? "all" : "any";
@@ -616,7 +866,7 @@ function parseForm(input) {
616
866
  return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
617
867
  }
618
868
  if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
619
- if (typeof c.op !== "string" || !isObject(c.left)) {
869
+ if (typeof c.op !== "string" || !isObject2(c.left)) {
620
870
  return issue(path, "A comparison needs an `op` and a `left` operand.");
621
871
  }
622
872
  if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
@@ -632,7 +882,7 @@ function parseForm(input) {
632
882
  };
633
883
  const checkLiterals = (op, left, right, path) => {
634
884
  if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
635
- const literal = isObject(right) && "value" in right;
885
+ const literal = isObject2(right) && "value" in right;
636
886
  if (right !== void 0 && !literal) return;
637
887
  let what = "the left side";
638
888
  let type;
@@ -671,7 +921,7 @@ function parseForm(input) {
671
921
  if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
672
922
  (Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
673
923
  const path = `logic[${i}]`;
674
- if (!isObject(rule) || typeof rule.id !== "string" || !isObject(rule.when)) {
924
+ if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
675
925
  issue(path, "Each rule needs an id and a `when` condition.");
676
926
  return;
677
927
  }
@@ -690,7 +940,7 @@ function parseForm(input) {
690
940
  at = rule.from;
691
941
  inclusive = true;
692
942
  }
693
- if (isObject(rule.to)) {
943
+ if (isObject2(rule.to)) {
694
944
  if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
695
945
  issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
696
946
  }
@@ -722,7 +972,7 @@ function parseForm(input) {
722
972
  }
723
973
  });
724
974
  input.blocks.forEach((block, i) => {
725
- if (!isObject(block)) return;
975
+ if (!isObject2(block)) return;
726
976
  const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
727
977
  const texts = raw.filter((t) => typeof t === "string");
728
978
  for (const t of texts) {
@@ -742,7 +992,7 @@ function parseForm(input) {
742
992
  });
743
993
  const extra = [["description", input.description]];
744
994
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
745
- if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
995
+ if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
746
996
  });
747
997
  for (const [path, t] of extra) {
748
998
  if (typeof t !== "string") continue;
@@ -756,7 +1006,7 @@ function parseForm(input) {
756
1006
  }
757
1007
 
758
1008
  // src/define.ts
759
- function field(type, key, options, props) {
1009
+ function field(type, key, options, props2) {
760
1010
  const block = {
761
1011
  kind: "field",
762
1012
  id: options.id ?? key,
@@ -769,9 +1019,10 @@ function field(type, key, options, props) {
769
1019
  if (options.required) block.required = true;
770
1020
  if (options.sensitive) block.sensitive = true;
771
1021
  if (options.identifying) block.identifying = true;
772
- if (props && Object.keys(props).length) block.props = props;
1022
+ if (props2 && Object.keys(props2).length) block.props = props2;
773
1023
  return block;
774
1024
  }
1025
+ var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
775
1026
  var choiceProps = (options) => ({
776
1027
  options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
777
1028
  });
@@ -791,6 +1042,41 @@ var f = {
791
1042
  singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
792
1043
  multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
793
1044
  yesNo: (key, o) => field("yes-no", key, o),
1045
+ /** Several short answers, one per line: goals, action items, three things that went well. */
1046
+ list: (key, o) => field(
1047
+ "list",
1048
+ key,
1049
+ o,
1050
+ props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
1051
+ ),
1052
+ /**
1053
+ * A small table each answer fills row by row, one answer per column: an
1054
+ * action plan (What / Who / By when), pros and cons.
1055
+ */
1056
+ table: (key, o) => field("table", key, o, {
1057
+ columns: o.columns.map((c) => c.long ? { key: c.key, label: c.label, long: true } : { key: c.key, label: c.label }),
1058
+ ...props({ minRows: o.minRows, maxRows: o.maxRows })
1059
+ }),
1060
+ /**
1061
+ * Notes pinned to times in a video or audio (lecture notes, a recorded call
1062
+ * review): `at` is seconds from its start.
1063
+ */
1064
+ mediaNotes: (key, o) => field(
1065
+ "media-notes",
1066
+ key,
1067
+ o,
1068
+ props({ maxNotes: o.maxNotes, textMaxLength: o.textMaxLength })
1069
+ ),
1070
+ /**
1071
+ * Highlight passages of a text and note on each (annotating an article, a
1072
+ * policy or a contract): `text` here, or supplied by the host app.
1073
+ */
1074
+ textHighlights: (key, o) => field(
1075
+ "text-highlights",
1076
+ key,
1077
+ o,
1078
+ props({ text: o.text, maxHighlights: o.maxHighlights })
1079
+ ),
794
1080
  /** Any registered field type, including plugins. */
795
1081
  custom: (type, key, o) => field(type, key, o, o.props),
796
1082
  heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
@@ -1261,8 +1547,15 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
1261
1547
  return `${left} ${op} ${right}`;
1262
1548
  }
1263
1549
 
1550
+ exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
1264
1551
  exports.MAX_HIDDEN_STRING_LENGTH = MAX_HIDDEN_STRING_LENGTH;
1552
+ exports.MAX_HIGHLIGHT_NOTE_LENGTH = MAX_HIGHLIGHT_NOTE_LENGTH;
1553
+ exports.MAX_HIGHLIGHT_QUOTE_LENGTH = MAX_HIGHLIGHT_QUOTE_LENGTH;
1554
+ exports.MAX_TABLE_CELL_LENGTH = MAX_TABLE_CELL_LENGTH;
1555
+ exports.MAX_TABLE_COLUMNS = MAX_TABLE_COLUMNS;
1556
+ exports.MAX_TIMESTAMP_SECONDS = MAX_TIMESTAMP_SECONDS;
1265
1557
  exports.REQUIRED_MESSAGE = REQUIRED_MESSAGE;
1558
+ exports.STRUCTURED_FIELD_TYPES = STRUCTURED_FIELD_TYPES;
1266
1559
  exports.canonicalJson = canonicalJson;
1267
1560
  exports.choiceOptions = choiceOptions;
1268
1561
  exports.coerceHidden = coerceHidden;
@@ -1275,20 +1568,25 @@ exports.evaluate = evaluate;
1275
1568
  exports.explain = explain;
1276
1569
  exports.f = f;
1277
1570
  exports.fingerprint = fingerprint;
1571
+ exports.formatTimestamp = formatTimestamp;
1278
1572
  exports.getFieldType = getFieldType;
1279
1573
  exports.isEmptyValue = isEmptyValue;
1280
1574
  exports.isFormEvent = isFormEvent;
1281
1575
  exports.isJson = isJson;
1282
1576
  exports.isSensitive = isSensitive;
1283
1577
  exports.listFieldTypes = listFieldTypes;
1578
+ exports.listLimits = listLimits;
1284
1579
  exports.normalizeValue = normalizeValue;
1285
1580
  exports.parseForm = parseForm;
1581
+ exports.parseTimestamp = parseTimestamp;
1286
1582
  exports.recall = recall;
1287
1583
  exports.registerFieldType = registerFieldType;
1288
1584
  exports.replay = replay;
1289
1585
  exports.ruleVisible = ruleVisible;
1290
1586
  exports.steps = steps;
1291
1587
  exports.summarizeValue = summarizeValue;
1588
+ exports.tableColumns = tableColumns;
1589
+ exports.tableLimits = tableLimits;
1292
1590
  exports.validateField = validateField;
1293
1591
  exports.when = when;
1294
1592
  //# sourceMappingURL=index.cjs.map