@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.cjs +206 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +208 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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__ */
|
|
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(
|
|
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({
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
}
|