@dataimago/interview 0.2.0-alpha.5 → 0.2.0-alpha.7

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.
package/dist/index.js CHANGED
@@ -38,8 +38,8 @@ function ProgressBar({ current, total, estimatedMinutesRemaining }) {
38
38
  }
39
39
 
40
40
  // src/QuestionCard.tsx
41
- import { useState, useEffect } from "react";
42
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
41
+ import { useState, useEffect, useId } from "react";
42
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
43
43
  function defaultValueFor(question) {
44
44
  switch (question.inputType) {
45
45
  case "multi-select":
@@ -62,6 +62,30 @@ function canAdvanceWith(question, value) {
62
62
  if (typeof value === "object") return Object.keys(value).length > 0;
63
63
  return true;
64
64
  }
65
+ function validationError(question, value) {
66
+ const v = question.validation;
67
+ if (!v) return null;
68
+ if (typeof value !== "string" || value.trim().length === 0) return null;
69
+ try {
70
+ return new RegExp(v.pattern).test(value) ? null : v.message;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function hasValidationError(question, value) {
76
+ if (validationError(question, value) !== null) return true;
77
+ if (question.inputType === "composite" && question.subQuestions) {
78
+ const rec = value ?? {};
79
+ return question.subQuestions.some((sub) => validationError(sub, rec[sub.id]) !== null);
80
+ }
81
+ if ((question.inputType === "repeater" || question.inputType === "file-upload") && question.itemSchema) {
82
+ const rows = value ?? [];
83
+ return rows.some(
84
+ (row) => question.itemSchema.some((sub) => validationError(sub, row[sub.id]) !== null)
85
+ );
86
+ }
87
+ return false;
88
+ }
65
89
  function QuestionCard({
66
90
  question,
67
91
  existingAnswer,
@@ -82,9 +106,12 @@ function QuestionCard({
82
106
  onAnswerSubmit(question.id, localValue);
83
107
  onNext();
84
108
  };
85
- const canAdvance = canAdvanceWith(question, localValue);
109
+ const canAdvance = canAdvanceWith(question, localValue) && !hasValidationError(question, localValue);
86
110
  return /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit, className: "animate-slide-up", children: [
87
- /* @__PURE__ */ jsx2("h2", { className: "font-display text-3xl font-medium text-stone-900 sm:text-4xl", children: question.prompt }),
111
+ /* @__PURE__ */ jsxs2("h2", { className: "font-display text-3xl font-medium text-stone-900 sm:text-4xl", children: [
112
+ question.prompt,
113
+ !question.required && /* @__PURE__ */ jsx2("span", { className: "ml-3 inline-block translate-y-[-0.2em] rounded-full border border-stone-300 bg-stone-100 px-3 py-1 align-middle font-sans text-xs font-medium uppercase tracking-wider text-stone-700", children: "Optional" })
114
+ ] }),
88
115
  question.subprompt && /* @__PURE__ */ jsx2("p", { className: "mt-4 text-lg text-stone-700", children: question.subprompt }),
89
116
  /* @__PURE__ */ jsx2("div", { className: "mt-8", children: /* @__PURE__ */ jsx2(
90
117
  InputForType,
@@ -128,7 +155,15 @@ function QuestionCard({
128
155
  function InputForType({ question, value, onChange }) {
129
156
  switch (question.inputType) {
130
157
  case "short-text":
131
- return /* @__PURE__ */ jsx2(ShortTextInput, { value, onChange });
158
+ return /* @__PURE__ */ jsx2(
159
+ ShortTextInput,
160
+ {
161
+ question,
162
+ value,
163
+ onChange
164
+ },
165
+ question.id
166
+ );
132
167
  case "long-text":
133
168
  return /* @__PURE__ */ jsx2(LongTextInput, { value, onChange });
134
169
  case "single-select":
@@ -153,18 +188,33 @@ function InputForType({ question, value, onChange }) {
153
188
  }
154
189
  }
155
190
  }
156
- function ShortTextInput({ value, onChange }) {
157
- return /* @__PURE__ */ jsx2(
158
- "input",
159
- {
160
- type: "text",
161
- value: value ?? "",
162
- onChange: (e) => onChange(e.target.value),
163
- autoFocus: true,
164
- className: "w-full rounded-lg border-2 border-stone-200 bg-stone-50 px-4 py-3 text-lg text-stone-900 focus:border-forest-700 focus:outline-none",
165
- placeholder: "Type your answer\u2026"
166
- }
167
- );
191
+ function ShortTextInput({
192
+ question,
193
+ value,
194
+ onChange
195
+ }) {
196
+ const [blurred, setBlurred] = useState(false);
197
+ const errorId = useId();
198
+ const error = question ? validationError(question, value) : null;
199
+ const showError = blurred && error !== null;
200
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
201
+ /* @__PURE__ */ jsx2(
202
+ "input",
203
+ {
204
+ type: "text",
205
+ value: value ?? "",
206
+ onChange: (e) => onChange(e.target.value),
207
+ onBlur: () => setBlurred(true),
208
+ autoFocus: true,
209
+ "aria-label": question?.prompt,
210
+ "aria-invalid": showError || void 0,
211
+ "aria-describedby": showError ? errorId : void 0,
212
+ className: `w-full rounded-lg border-2 bg-stone-50 px-4 py-3 text-lg text-stone-900 focus:outline-none ${showError ? "border-red-400 focus:border-red-700" : "border-stone-200 focus:border-forest-700"}`,
213
+ placeholder: "Type your answer\u2026"
214
+ }
215
+ ),
216
+ showError && /* @__PURE__ */ jsx2("p", { id: errorId, className: "mt-2 text-sm font-medium text-red-800", children: error })
217
+ ] });
168
218
  }
169
219
  function LongTextInput({ value, onChange }) {
170
220
  return /* @__PURE__ */ jsx2(
@@ -379,6 +429,23 @@ function CompositeInput({
379
429
  ) })
380
430
  ] }, sub.id)) });
381
431
  }
432
+ function isRowComplete(itemSchema, row) {
433
+ return itemSchema.every((sub) => {
434
+ if (!sub.required) return true;
435
+ const v = row[sub.id];
436
+ return typeof v === "string" ? v.trim().length > 0 : v !== void 0 && v !== null;
437
+ });
438
+ }
439
+ function summarizeRow(question, itemSchema, row) {
440
+ const stringValue = (id) => {
441
+ const v = row[id];
442
+ return typeof v === "string" ? v.trim() : "";
443
+ };
444
+ let primaryId = question.summaryField && stringValue(question.summaryField) ? question.summaryField : itemSchema.find((sub) => stringValue(sub.id))?.id;
445
+ const primary = primaryId ? stringValue(primaryId) : "";
446
+ const secondary = itemSchema.filter((sub) => sub.id !== primaryId).map((sub) => stringValue(sub.id)).filter(Boolean).join(" \xB7 ");
447
+ return { primary, secondary: secondary.length > 80 ? `${secondary.slice(0, 80)}\u2026` : secondary };
448
+ }
382
449
  function RepeaterInput({
383
450
  question,
384
451
  value,
@@ -388,60 +455,137 @@ function RepeaterInput({
388
455
  const itemSchema = question.itemSchema ?? [];
389
456
  const min = question.listRange?.min ?? 1;
390
457
  const max = question.listRange?.max ?? Infinity;
458
+ const [activeRow, setActiveRow] = useState(() => {
459
+ const firstIncomplete = rows.findIndex((row) => !isRowComplete(itemSchema, row));
460
+ return firstIncomplete === -1 ? null : firstIncomplete;
461
+ });
391
462
  const updateRow = (i, patch) => {
392
463
  const next = [...rows];
393
464
  next[i] = { ...next[i], ...patch };
394
465
  onChange(next);
395
466
  };
396
467
  const addRow = () => {
397
- if (rows.length < max) onChange([...rows, {}]);
468
+ if (rows.length >= max) return;
469
+ onChange([...rows, {}]);
470
+ setActiveRow(rows.length);
398
471
  };
399
472
  const removeRow = (i) => {
400
473
  if (rows.length <= 1) return;
401
474
  onChange(rows.filter((_, idx) => idx !== i));
475
+ setActiveRow((a) => a === null ? null : a === i ? null : a > i ? a - 1 : a);
402
476
  };
403
- return /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
404
- rows.map((row, i) => /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border-2 border-stone-200 bg-stone-50 p-4", children: [
405
- /* @__PURE__ */ jsxs2("div", { className: "mb-3 flex items-center justify-between", children: [
406
- /* @__PURE__ */ jsxs2("span", { className: "font-mono text-xs uppercase tracking-wider text-stone-700", children: [
407
- "Item ",
408
- i + 1
409
- ] }),
410
- /* @__PURE__ */ jsx2(
411
- "button",
412
- {
413
- type: "button",
414
- onClick: () => removeRow(i),
415
- disabled: rows.length <= min,
416
- className: "btn-ghost text-xs text-stone-700 disabled:opacity-30",
417
- "aria-label": `Remove item ${i + 1}`,
418
- children: "Remove"
419
- }
420
- )
421
- ] }),
422
- /* @__PURE__ */ jsx2("div", { className: "space-y-4", children: itemSchema.map((sub) => /* @__PURE__ */ jsxs2("div", { children: [
423
- /* @__PURE__ */ jsx2("div", { className: "text-sm font-medium text-stone-900", children: sub.prompt }),
424
- sub.subprompt && /* @__PURE__ */ jsx2("div", { className: "mt-1 text-xs text-stone-700", children: sub.subprompt }),
425
- /* @__PURE__ */ jsx2("div", { className: "mt-2", children: /* @__PURE__ */ jsx2(
426
- InputForType,
477
+ const completeCount = rows.filter((row) => isRowComplete(itemSchema, row)).length;
478
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-3", children: [
479
+ rows.map((row, i) => {
480
+ const complete = isRowComplete(itemSchema, row);
481
+ if (i !== activeRow) {
482
+ const { primary, secondary } = summarizeRow(question, itemSchema, row);
483
+ return /* @__PURE__ */ jsxs2(
484
+ "div",
427
485
  {
428
- question: sub,
429
- value: row[sub.id],
430
- onChange: (next) => updateRow(i, { [sub.id]: next })
431
- }
432
- ) })
433
- ] }, sub.id)) })
434
- ] }, i)),
435
- /* @__PURE__ */ jsx2(
436
- "button",
437
- {
438
- type: "button",
439
- onClick: addRow,
440
- disabled: rows.length >= max,
441
- className: "inline-flex min-h-12 items-center px-2 text-sm font-medium text-forest-700 hover:text-forest-800 disabled:opacity-40",
442
- children: "+ Add another"
486
+ className: "flex items-center justify-between gap-3 rounded-lg border-2 border-stone-200 bg-stone-50 px-4 py-2",
487
+ children: [
488
+ /* @__PURE__ */ jsxs2("div", { className: "min-w-0", children: [
489
+ /* @__PURE__ */ jsx2("div", { className: "truncate text-sm font-medium text-stone-900", children: primary || /* @__PURE__ */ jsx2("span", { className: "italic text-stone-600", children: "Empty entry" }) }),
490
+ (secondary || !complete) && /* @__PURE__ */ jsxs2("div", { className: "truncate text-xs text-stone-700", children: [
491
+ secondary,
492
+ !complete && /* @__PURE__ */ jsxs2("span", { className: "font-medium text-amber-800", children: [
493
+ secondary ? " \xB7 " : "",
494
+ "incomplete"
495
+ ] })
496
+ ] })
497
+ ] }),
498
+ /* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center", children: [
499
+ /* @__PURE__ */ jsx2(
500
+ "button",
501
+ {
502
+ type: "button",
503
+ onClick: () => setActiveRow(i),
504
+ className: "btn-ghost text-xs text-stone-700",
505
+ "aria-label": `Edit entry ${i + 1}${primary ? `: ${primary}` : ""}`,
506
+ children: "Edit"
507
+ }
508
+ ),
509
+ /* @__PURE__ */ jsx2(
510
+ "button",
511
+ {
512
+ type: "button",
513
+ onClick: () => removeRow(i),
514
+ disabled: rows.length <= min,
515
+ className: "btn-ghost text-xs text-stone-700 disabled:opacity-30",
516
+ "aria-label": `Remove entry ${i + 1}${primary ? `: ${primary}` : ""}`,
517
+ children: "Remove"
518
+ }
519
+ )
520
+ ] })
521
+ ]
522
+ },
523
+ i
524
+ );
443
525
  }
444
- )
526
+ return /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border-2 border-forest-700 bg-stone-50 p-4", children: [
527
+ /* @__PURE__ */ jsxs2("div", { className: "mb-3 flex items-center justify-between", children: [
528
+ /* @__PURE__ */ jsxs2("span", { className: "font-mono text-xs uppercase tracking-wider text-stone-700", children: [
529
+ "Entry ",
530
+ i + 1
531
+ ] }),
532
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center", children: [
533
+ /* @__PURE__ */ jsx2(
534
+ "button",
535
+ {
536
+ type: "button",
537
+ onClick: () => setActiveRow(null),
538
+ className: "btn-ghost text-xs text-stone-700",
539
+ children: "Done"
540
+ }
541
+ ),
542
+ /* @__PURE__ */ jsx2(
543
+ "button",
544
+ {
545
+ type: "button",
546
+ onClick: () => removeRow(i),
547
+ disabled: rows.length <= min,
548
+ className: "btn-ghost text-xs text-stone-700 disabled:opacity-30",
549
+ "aria-label": `Remove entry ${i + 1}`,
550
+ children: "Remove"
551
+ }
552
+ )
553
+ ] })
554
+ ] }),
555
+ /* @__PURE__ */ jsx2("div", { className: "space-y-4", children: itemSchema.map((sub) => /* @__PURE__ */ jsxs2("div", { children: [
556
+ /* @__PURE__ */ jsx2("div", { className: "text-sm font-medium text-stone-900", children: sub.prompt }),
557
+ sub.subprompt && /* @__PURE__ */ jsx2("div", { className: "mt-1 text-xs text-stone-700", children: sub.subprompt }),
558
+ /* @__PURE__ */ jsx2("div", { className: "mt-2", children: /* @__PURE__ */ jsx2(
559
+ InputForType,
560
+ {
561
+ question: sub,
562
+ value: row[sub.id],
563
+ onChange: (next) => updateRow(i, { [sub.id]: next })
564
+ }
565
+ ) })
566
+ ] }, sub.id)) })
567
+ ] }, i);
568
+ }),
569
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
570
+ /* @__PURE__ */ jsx2(
571
+ "button",
572
+ {
573
+ type: "button",
574
+ onClick: addRow,
575
+ disabled: rows.length >= max,
576
+ className: "inline-flex min-h-12 items-center px-2 text-sm font-medium text-forest-700 hover:text-forest-800 disabled:opacity-40",
577
+ children: "+ Add another"
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsxs2("p", { "aria-live": "polite", className: "font-mono text-xs text-stone-700", children: [
581
+ completeCount,
582
+ " of ",
583
+ rows.length,
584
+ " ",
585
+ rows.length === 1 ? "entry" : "entries",
586
+ " complete"
587
+ ] })
588
+ ] })
445
589
  ] });
446
590
  }
447
591
  function FileUploadInput({
@@ -694,7 +838,12 @@ function AnswerSidebar({ questions, currentIndex, answers, onSelectQuestion }) {
694
838
  children: q.prompt.length > 50 ? q.prompt.slice(0, 50) + "\u2026" : q.prompt
695
839
  }
696
840
  ),
697
- answered && /* @__PURE__ */ jsx3("div", { className: "mt-1 text-xs italic text-stone-700 line-clamp-1", children: preview })
841
+ answered && /* @__PURE__ */ jsx3("div", { className: "mt-1 text-xs italic text-stone-700 line-clamp-1", children: preview }),
842
+ !answered && q.required && /* @__PURE__ */ jsxs3("div", { className: "mt-1 text-xs font-medium text-amber-800", children: [
843
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u25CF" }),
844
+ " Required \u2014 not yet answered"
845
+ ] }),
846
+ !answered && !q.required && /* @__PURE__ */ jsx3("div", { className: "mt-1 text-xs text-stone-600", children: "Optional" })
698
847
  ] })
699
848
  ] })
700
849
  }