@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.cjs
CHANGED
|
@@ -94,6 +94,30 @@ function canAdvanceWith(question, value) {
|
|
|
94
94
|
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
95
95
|
return true;
|
|
96
96
|
}
|
|
97
|
+
function validationError(question, value) {
|
|
98
|
+
const v = question.validation;
|
|
99
|
+
if (!v) return null;
|
|
100
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
101
|
+
try {
|
|
102
|
+
return new RegExp(v.pattern).test(value) ? null : v.message;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function hasValidationError(question, value) {
|
|
108
|
+
if (validationError(question, value) !== null) return true;
|
|
109
|
+
if (question.inputType === "composite" && question.subQuestions) {
|
|
110
|
+
const rec = value ?? {};
|
|
111
|
+
return question.subQuestions.some((sub) => validationError(sub, rec[sub.id]) !== null);
|
|
112
|
+
}
|
|
113
|
+
if ((question.inputType === "repeater" || question.inputType === "file-upload") && question.itemSchema) {
|
|
114
|
+
const rows = value ?? [];
|
|
115
|
+
return rows.some(
|
|
116
|
+
(row) => question.itemSchema.some((sub) => validationError(sub, row[sub.id]) !== null)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
97
121
|
function QuestionCard({
|
|
98
122
|
question,
|
|
99
123
|
existingAnswer,
|
|
@@ -114,9 +138,12 @@ function QuestionCard({
|
|
|
114
138
|
onAnswerSubmit(question.id, localValue);
|
|
115
139
|
onNext();
|
|
116
140
|
};
|
|
117
|
-
const canAdvance = canAdvanceWith(question, localValue);
|
|
141
|
+
const canAdvance = canAdvanceWith(question, localValue) && !hasValidationError(question, localValue);
|
|
118
142
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleSubmit, className: "animate-slide-up", children: [
|
|
119
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("h2", { className: "font-display text-3xl font-medium text-stone-900 sm:text-4xl", children: [
|
|
144
|
+
question.prompt,
|
|
145
|
+
!question.required && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" })
|
|
146
|
+
] }),
|
|
120
147
|
question.subprompt && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mt-4 text-lg text-stone-700", children: question.subprompt }),
|
|
121
148
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-8", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
122
149
|
InputForType,
|
|
@@ -160,7 +187,15 @@ function QuestionCard({
|
|
|
160
187
|
function InputForType({ question, value, onChange }) {
|
|
161
188
|
switch (question.inputType) {
|
|
162
189
|
case "short-text":
|
|
163
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
190
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
191
|
+
ShortTextInput,
|
|
192
|
+
{
|
|
193
|
+
question,
|
|
194
|
+
value,
|
|
195
|
+
onChange
|
|
196
|
+
},
|
|
197
|
+
question.id
|
|
198
|
+
);
|
|
164
199
|
case "long-text":
|
|
165
200
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LongTextInput, { value, onChange });
|
|
166
201
|
case "single-select":
|
|
@@ -185,18 +220,33 @@ function InputForType({ question, value, onChange }) {
|
|
|
185
220
|
}
|
|
186
221
|
}
|
|
187
222
|
}
|
|
188
|
-
function ShortTextInput({
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
function ShortTextInput({
|
|
224
|
+
question,
|
|
225
|
+
value,
|
|
226
|
+
onChange
|
|
227
|
+
}) {
|
|
228
|
+
const [blurred, setBlurred] = (0, import_react.useState)(false);
|
|
229
|
+
const errorId = (0, import_react.useId)();
|
|
230
|
+
const error = question ? validationError(question, value) : null;
|
|
231
|
+
const showError = blurred && error !== null;
|
|
232
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
233
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
234
|
+
"input",
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
value: value ?? "",
|
|
238
|
+
onChange: (e) => onChange(e.target.value),
|
|
239
|
+
onBlur: () => setBlurred(true),
|
|
240
|
+
autoFocus: true,
|
|
241
|
+
"aria-label": question?.prompt,
|
|
242
|
+
"aria-invalid": showError || void 0,
|
|
243
|
+
"aria-describedby": showError ? errorId : void 0,
|
|
244
|
+
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"}`,
|
|
245
|
+
placeholder: "Type your answer\u2026"
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
showError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: errorId, className: "mt-2 text-sm font-medium text-red-800", children: error })
|
|
249
|
+
] });
|
|
200
250
|
}
|
|
201
251
|
function LongTextInput({ value, onChange }) {
|
|
202
252
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
@@ -411,6 +461,23 @@ function CompositeInput({
|
|
|
411
461
|
) })
|
|
412
462
|
] }, sub.id)) });
|
|
413
463
|
}
|
|
464
|
+
function isRowComplete(itemSchema, row) {
|
|
465
|
+
return itemSchema.every((sub) => {
|
|
466
|
+
if (!sub.required) return true;
|
|
467
|
+
const v = row[sub.id];
|
|
468
|
+
return typeof v === "string" ? v.trim().length > 0 : v !== void 0 && v !== null;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function summarizeRow(question, itemSchema, row) {
|
|
472
|
+
const stringValue = (id) => {
|
|
473
|
+
const v = row[id];
|
|
474
|
+
return typeof v === "string" ? v.trim() : "";
|
|
475
|
+
};
|
|
476
|
+
let primaryId = question.summaryField && stringValue(question.summaryField) ? question.summaryField : itemSchema.find((sub) => stringValue(sub.id))?.id;
|
|
477
|
+
const primary = primaryId ? stringValue(primaryId) : "";
|
|
478
|
+
const secondary = itemSchema.filter((sub) => sub.id !== primaryId).map((sub) => stringValue(sub.id)).filter(Boolean).join(" \xB7 ");
|
|
479
|
+
return { primary, secondary: secondary.length > 80 ? `${secondary.slice(0, 80)}\u2026` : secondary };
|
|
480
|
+
}
|
|
414
481
|
function RepeaterInput({
|
|
415
482
|
question,
|
|
416
483
|
value,
|
|
@@ -420,60 +487,137 @@ function RepeaterInput({
|
|
|
420
487
|
const itemSchema = question.itemSchema ?? [];
|
|
421
488
|
const min = question.listRange?.min ?? 1;
|
|
422
489
|
const max = question.listRange?.max ?? Infinity;
|
|
490
|
+
const [activeRow, setActiveRow] = (0, import_react.useState)(() => {
|
|
491
|
+
const firstIncomplete = rows.findIndex((row) => !isRowComplete(itemSchema, row));
|
|
492
|
+
return firstIncomplete === -1 ? null : firstIncomplete;
|
|
493
|
+
});
|
|
423
494
|
const updateRow = (i, patch) => {
|
|
424
495
|
const next = [...rows];
|
|
425
496
|
next[i] = { ...next[i], ...patch };
|
|
426
497
|
onChange(next);
|
|
427
498
|
};
|
|
428
499
|
const addRow = () => {
|
|
429
|
-
if (rows.length
|
|
500
|
+
if (rows.length >= max) return;
|
|
501
|
+
onChange([...rows, {}]);
|
|
502
|
+
setActiveRow(rows.length);
|
|
430
503
|
};
|
|
431
504
|
const removeRow = (i) => {
|
|
432
505
|
if (rows.length <= 1) return;
|
|
433
506
|
onChange(rows.filter((_, idx) => idx !== i));
|
|
507
|
+
setActiveRow((a) => a === null ? null : a === i ? null : a > i ? a - 1 : a);
|
|
434
508
|
};
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"button",
|
|
444
|
-
{
|
|
445
|
-
type: "button",
|
|
446
|
-
onClick: () => removeRow(i),
|
|
447
|
-
disabled: rows.length <= min,
|
|
448
|
-
className: "btn-ghost text-xs text-stone-700 disabled:opacity-30",
|
|
449
|
-
"aria-label": `Remove item ${i + 1}`,
|
|
450
|
-
children: "Remove"
|
|
451
|
-
}
|
|
452
|
-
)
|
|
453
|
-
] }),
|
|
454
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "space-y-4", children: itemSchema.map((sub) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
|
|
455
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-sm font-medium text-stone-900", children: sub.prompt }),
|
|
456
|
-
sub.subprompt && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-1 text-xs text-stone-700", children: sub.subprompt }),
|
|
457
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
458
|
-
InputForType,
|
|
509
|
+
const completeCount = rows.filter((row) => isRowComplete(itemSchema, row)).length;
|
|
510
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-3", children: [
|
|
511
|
+
rows.map((row, i) => {
|
|
512
|
+
const complete = isRowComplete(itemSchema, row);
|
|
513
|
+
if (i !== activeRow) {
|
|
514
|
+
const { primary, secondary } = summarizeRow(question, itemSchema, row);
|
|
515
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
516
|
+
"div",
|
|
459
517
|
{
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
518
|
+
className: "flex items-center justify-between gap-3 rounded-lg border-2 border-stone-200 bg-stone-50 px-4 py-2",
|
|
519
|
+
children: [
|
|
520
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
|
|
521
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "truncate text-sm font-medium text-stone-900", children: primary || /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "italic text-stone-600", children: "Empty entry" }) }),
|
|
522
|
+
(secondary || !complete) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "truncate text-xs text-stone-700", children: [
|
|
523
|
+
secondary,
|
|
524
|
+
!complete && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-medium text-amber-800", children: [
|
|
525
|
+
secondary ? " \xB7 " : "",
|
|
526
|
+
"incomplete"
|
|
527
|
+
] })
|
|
528
|
+
] })
|
|
529
|
+
] }),
|
|
530
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex shrink-0 items-center", children: [
|
|
531
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
532
|
+
"button",
|
|
533
|
+
{
|
|
534
|
+
type: "button",
|
|
535
|
+
onClick: () => setActiveRow(i),
|
|
536
|
+
className: "btn-ghost text-xs text-stone-700",
|
|
537
|
+
"aria-label": `Edit entry ${i + 1}${primary ? `: ${primary}` : ""}`,
|
|
538
|
+
children: "Edit"
|
|
539
|
+
}
|
|
540
|
+
),
|
|
541
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
542
|
+
"button",
|
|
543
|
+
{
|
|
544
|
+
type: "button",
|
|
545
|
+
onClick: () => removeRow(i),
|
|
546
|
+
disabled: rows.length <= min,
|
|
547
|
+
className: "btn-ghost text-xs text-stone-700 disabled:opacity-30",
|
|
548
|
+
"aria-label": `Remove entry ${i + 1}${primary ? `: ${primary}` : ""}`,
|
|
549
|
+
children: "Remove"
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
] })
|
|
553
|
+
]
|
|
554
|
+
},
|
|
555
|
+
i
|
|
556
|
+
);
|
|
475
557
|
}
|
|
476
|
-
|
|
558
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "rounded-lg border-2 border-forest-700 bg-stone-50 p-4", children: [
|
|
559
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mb-3 flex items-center justify-between", children: [
|
|
560
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-mono text-xs uppercase tracking-wider text-stone-700", children: [
|
|
561
|
+
"Entry ",
|
|
562
|
+
i + 1
|
|
563
|
+
] }),
|
|
564
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center", children: [
|
|
565
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
566
|
+
"button",
|
|
567
|
+
{
|
|
568
|
+
type: "button",
|
|
569
|
+
onClick: () => setActiveRow(null),
|
|
570
|
+
className: "btn-ghost text-xs text-stone-700",
|
|
571
|
+
children: "Done"
|
|
572
|
+
}
|
|
573
|
+
),
|
|
574
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
575
|
+
"button",
|
|
576
|
+
{
|
|
577
|
+
type: "button",
|
|
578
|
+
onClick: () => removeRow(i),
|
|
579
|
+
disabled: rows.length <= min,
|
|
580
|
+
className: "btn-ghost text-xs text-stone-700 disabled:opacity-30",
|
|
581
|
+
"aria-label": `Remove entry ${i + 1}`,
|
|
582
|
+
children: "Remove"
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
] })
|
|
586
|
+
] }),
|
|
587
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "space-y-4", children: itemSchema.map((sub) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
|
|
588
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-sm font-medium text-stone-900", children: sub.prompt }),
|
|
589
|
+
sub.subprompt && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-1 text-xs text-stone-700", children: sub.subprompt }),
|
|
590
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
591
|
+
InputForType,
|
|
592
|
+
{
|
|
593
|
+
question: sub,
|
|
594
|
+
value: row[sub.id],
|
|
595
|
+
onChange: (next) => updateRow(i, { [sub.id]: next })
|
|
596
|
+
}
|
|
597
|
+
) })
|
|
598
|
+
] }, sub.id)) })
|
|
599
|
+
] }, i);
|
|
600
|
+
}),
|
|
601
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between", children: [
|
|
602
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
603
|
+
"button",
|
|
604
|
+
{
|
|
605
|
+
type: "button",
|
|
606
|
+
onClick: addRow,
|
|
607
|
+
disabled: rows.length >= max,
|
|
608
|
+
className: "inline-flex min-h-12 items-center px-2 text-sm font-medium text-forest-700 hover:text-forest-800 disabled:opacity-40",
|
|
609
|
+
children: "+ Add another"
|
|
610
|
+
}
|
|
611
|
+
),
|
|
612
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { "aria-live": "polite", className: "font-mono text-xs text-stone-700", children: [
|
|
613
|
+
completeCount,
|
|
614
|
+
" of ",
|
|
615
|
+
rows.length,
|
|
616
|
+
" ",
|
|
617
|
+
rows.length === 1 ? "entry" : "entries",
|
|
618
|
+
" complete"
|
|
619
|
+
] })
|
|
620
|
+
] })
|
|
477
621
|
] });
|
|
478
622
|
}
|
|
479
623
|
function FileUploadInput({
|
|
@@ -726,7 +870,12 @@ function AnswerSidebar({ questions, currentIndex, answers, onSelectQuestion }) {
|
|
|
726
870
|
children: q.prompt.length > 50 ? q.prompt.slice(0, 50) + "\u2026" : q.prompt
|
|
727
871
|
}
|
|
728
872
|
),
|
|
729
|
-
answered && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-1 text-xs italic text-stone-700 line-clamp-1", children: preview })
|
|
873
|
+
answered && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-1 text-xs italic text-stone-700 line-clamp-1", children: preview }),
|
|
874
|
+
!answered && q.required && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mt-1 text-xs font-medium text-amber-800", children: [
|
|
875
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "aria-hidden": "true", children: "\u25CF" }),
|
|
876
|
+
" Required \u2014 not yet answered"
|
|
877
|
+
] }),
|
|
878
|
+
!answered && !q.required && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-1 text-xs text-stone-600", children: "Optional" })
|
|
730
879
|
] })
|
|
731
880
|
] })
|
|
732
881
|
}
|