@sunboard/react 0.1.3 → 0.2.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.
- package/dist/index.d.mts +70 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +364 -25
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +773 -560
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -142,7 +142,10 @@ function isAllowedLocalPreviewUrl(url) {
|
|
|
142
142
|
function isSunboardSpecLike(value) {
|
|
143
143
|
if (!value || typeof value !== "object") return false;
|
|
144
144
|
const candidate = value;
|
|
145
|
-
|
|
145
|
+
if (typeof candidate.key !== "string" || typeof candidate.name !== "string" || typeof candidate.type !== "string") return false;
|
|
146
|
+
const hasChecklist = typeof candidate.checklist === "object" && Array.isArray(candidate.checklist?.steps);
|
|
147
|
+
const hasSurvey = typeof candidate.survey === "object" && Array.isArray(candidate.survey?.steps);
|
|
148
|
+
return hasChecklist || hasSurvey;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
//#endregion
|
|
@@ -163,27 +166,44 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
163
166
|
sdkVersion: SDK_VERSION
|
|
164
167
|
});
|
|
165
168
|
}, [apiUrl, publishableKey]);
|
|
169
|
+
const [bootstrapNonce, setBootstrapNonce] = useState(0);
|
|
170
|
+
const refreshBootstrap = useCallback(() => {
|
|
171
|
+
setBootstrapNonce((nonce) => nonce + 1);
|
|
172
|
+
}, []);
|
|
166
173
|
const bootstrap = useSunboardBootstrap(client, {
|
|
167
174
|
enabled: !specProp && preview.source !== "preview" && !preview.loading,
|
|
168
175
|
environment,
|
|
176
|
+
nonce: bootstrapNonce,
|
|
169
177
|
origin: currentOrigin,
|
|
170
178
|
route: currentRoute,
|
|
171
179
|
user
|
|
172
180
|
});
|
|
173
181
|
const activeExperience = bootstrap.experience;
|
|
174
|
-
const
|
|
182
|
+
const activeSurvey = bootstrap.survey;
|
|
183
|
+
const hostedSpec = activeSurvey?.spec ?? activeExperience?.spec ?? null;
|
|
175
184
|
const resolvedSpec = specProp ?? preview.spec ?? hostedSpec;
|
|
176
185
|
const resolvedLoading = loadingProp ?? (preview.loading || bootstrap.loading);
|
|
177
186
|
const resolvedMode = modeProp ?? (preview.source === "preview" ? "preview" : "production");
|
|
178
187
|
const hostedMode = client !== null && !specProp && preview.source !== "preview";
|
|
188
|
+
const ownedAttributes = bootstrap.attributes;
|
|
189
|
+
const effectiveUser = useMemo(() => {
|
|
190
|
+
if (!user || !ownedAttributes || Object.keys(ownedAttributes).length === 0) return user;
|
|
191
|
+
return {
|
|
192
|
+
...user,
|
|
193
|
+
properties: {
|
|
194
|
+
...user.properties ?? {},
|
|
195
|
+
...ownedAttributes
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}, [user, ownedAttributes]);
|
|
179
199
|
const [completedStepKeys, setCompletedStepKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
180
200
|
const [completedTourKeys, setCompletedTourKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
181
201
|
const [activeTourState, setActiveTourState] = useState(null);
|
|
182
202
|
const [tourError, setTourError] = useState(null);
|
|
183
203
|
const lastNavigatedTourStep = useRef(null);
|
|
184
204
|
const storageKey = !hostedMode && resolvedSpec && user ? completedToursStorageKey(resolvedSpec.key, user.id) : null;
|
|
185
|
-
const experienceKey = activeExperience?.key ?? resolvedSpec?.key ?? null;
|
|
186
|
-
const versionId = activeExperience?.version.id ?? null;
|
|
205
|
+
const experienceKey = activeSurvey?.key ?? activeExperience?.key ?? resolvedSpec?.key ?? null;
|
|
206
|
+
const versionId = activeSurvey?.version.id ?? activeExperience?.version.id ?? null;
|
|
187
207
|
useEffect(() => {
|
|
188
208
|
setCompletedStepKeys(/* @__PURE__ */ new Set());
|
|
189
209
|
setActiveTourState(null);
|
|
@@ -221,7 +241,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
221
241
|
]);
|
|
222
242
|
useEffect(() => {
|
|
223
243
|
if (!resolvedSpec) return;
|
|
224
|
-
const tourCompletedStepKeys = resolvedSpec.checklist
|
|
244
|
+
const tourCompletedStepKeys = (resolvedSpec.checklist?.steps ?? []).filter((step) => step.completeWhen?.tour && effectiveCompletedTourKeys.has(step.completeWhen.tour)).map((step) => step.key);
|
|
225
245
|
if (tourCompletedStepKeys.length === 0) return;
|
|
226
246
|
setCompletedStepKeys((current) => mergeKeys(current, tourCompletedStepKeys));
|
|
227
247
|
}, [effectiveCompletedTourKeys, resolvedSpec]);
|
|
@@ -270,8 +290,8 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
270
290
|
hostedMode,
|
|
271
291
|
user?.id
|
|
272
292
|
]);
|
|
273
|
-
const totalSteps = resolvedSpec?.checklist
|
|
274
|
-
const completedCount = resolvedSpec?.checklist
|
|
293
|
+
const totalSteps = resolvedSpec?.checklist?.steps.length ?? 0;
|
|
294
|
+
const completedCount = resolvedSpec?.checklist?.steps.filter((step) => effectiveCompletedStepKeys.has(step.key)).length ?? 0;
|
|
275
295
|
const completeTour = useCallback((tourKey) => {
|
|
276
296
|
setCompletedTourKeys((current) => {
|
|
277
297
|
const next = new Set(current);
|
|
@@ -289,7 +309,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
289
309
|
ok: false,
|
|
290
310
|
reason: "missing-user"
|
|
291
311
|
};
|
|
292
|
-
const matchingStepKeys = resolvedSpec.checklist
|
|
312
|
+
const matchingStepKeys = (resolvedSpec.checklist?.steps ?? []).filter((step) => step.completeWhen?.event === eventName).map((step) => step.key);
|
|
293
313
|
if (matchingStepKeys.length > 0) setCompletedStepKeys((current) => mergeKeys(current, matchingStepKeys));
|
|
294
314
|
let networkOk = true;
|
|
295
315
|
if (hostedMode && client && experienceKey) try {
|
|
@@ -332,6 +352,56 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
332
352
|
user,
|
|
333
353
|
versionId
|
|
334
354
|
]);
|
|
355
|
+
const submitSurvey = useCallback(async (answers) => {
|
|
356
|
+
if (resolvedLoading || !resolvedSpec) return {
|
|
357
|
+
ok: false,
|
|
358
|
+
reason: "not-ready"
|
|
359
|
+
};
|
|
360
|
+
if (!user) return {
|
|
361
|
+
ok: false,
|
|
362
|
+
reason: "missing-user"
|
|
363
|
+
};
|
|
364
|
+
if (resolvedSpec.type !== "survey" || !resolvedSpec.survey) return {
|
|
365
|
+
ok: false,
|
|
366
|
+
reason: "not-survey"
|
|
367
|
+
};
|
|
368
|
+
if (resolvedMode === "preview") return { ok: true };
|
|
369
|
+
if (!hostedMode || !client || !experienceKey) return {
|
|
370
|
+
ok: false,
|
|
371
|
+
reason: "not-ready"
|
|
372
|
+
};
|
|
373
|
+
try {
|
|
374
|
+
await client.submitSurvey({
|
|
375
|
+
answers,
|
|
376
|
+
environment,
|
|
377
|
+
experienceKey,
|
|
378
|
+
origin: currentOrigin,
|
|
379
|
+
route: currentRoute,
|
|
380
|
+
user: toRuntimeUser(user),
|
|
381
|
+
versionId: versionId ?? void 0
|
|
382
|
+
});
|
|
383
|
+
} catch {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
reason: "network-error"
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
refreshBootstrap();
|
|
390
|
+
return { ok: true };
|
|
391
|
+
}, [
|
|
392
|
+
client,
|
|
393
|
+
currentOrigin,
|
|
394
|
+
currentRoute,
|
|
395
|
+
environment,
|
|
396
|
+
experienceKey,
|
|
397
|
+
hostedMode,
|
|
398
|
+
refreshBootstrap,
|
|
399
|
+
resolvedLoading,
|
|
400
|
+
resolvedMode,
|
|
401
|
+
resolvedSpec,
|
|
402
|
+
user,
|
|
403
|
+
versionId
|
|
404
|
+
]);
|
|
335
405
|
const navigateToStep = useCallback(async (step) => {
|
|
336
406
|
if (!step.route) return;
|
|
337
407
|
await navigateWithFallback(step.route, navigate);
|
|
@@ -486,7 +556,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
486
556
|
}, []);
|
|
487
557
|
const value = useMemo(() => ({
|
|
488
558
|
spec: resolvedSpec,
|
|
489
|
-
user,
|
|
559
|
+
user: effectiveUser,
|
|
490
560
|
loading: resolvedLoading,
|
|
491
561
|
mode: resolvedMode,
|
|
492
562
|
route: currentRoute,
|
|
@@ -497,6 +567,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
497
567
|
activeTour,
|
|
498
568
|
tourError,
|
|
499
569
|
track,
|
|
570
|
+
submitSurvey,
|
|
500
571
|
startTour,
|
|
501
572
|
dismissTour,
|
|
502
573
|
advanceTour,
|
|
@@ -506,7 +577,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
506
577
|
visibleHotspots
|
|
507
578
|
}), [
|
|
508
579
|
resolvedSpec,
|
|
509
|
-
|
|
580
|
+
effectiveUser,
|
|
510
581
|
resolvedLoading,
|
|
511
582
|
resolvedMode,
|
|
512
583
|
currentRoute,
|
|
@@ -517,6 +588,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
517
588
|
activeTour,
|
|
518
589
|
tourError,
|
|
519
590
|
track,
|
|
591
|
+
submitSurvey,
|
|
520
592
|
startTour,
|
|
521
593
|
dismissTour,
|
|
522
594
|
advanceTour,
|
|
@@ -531,24 +603,30 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
531
603
|
});
|
|
532
604
|
}
|
|
533
605
|
function useSunboardBootstrap(client, options) {
|
|
534
|
-
const { enabled, environment, origin, route, user } = options;
|
|
606
|
+
const { enabled, environment, nonce, origin, route, user } = options;
|
|
535
607
|
const [state, setState] = useState({
|
|
608
|
+
attributes: null,
|
|
536
609
|
experience: null,
|
|
537
|
-
loading: false
|
|
610
|
+
loading: false,
|
|
611
|
+
survey: null
|
|
538
612
|
});
|
|
539
613
|
const warnedSdkAdvisory = useRef(false);
|
|
540
614
|
useEffect(() => {
|
|
541
|
-
if (!client || !enabled || !user?.id) {
|
|
615
|
+
if (!client || !enabled || !user?.id || isPreviewPage()) {
|
|
542
616
|
setState({
|
|
617
|
+
attributes: null,
|
|
543
618
|
experience: null,
|
|
544
|
-
loading: false
|
|
619
|
+
loading: false,
|
|
620
|
+
survey: null
|
|
545
621
|
});
|
|
546
622
|
return;
|
|
547
623
|
}
|
|
548
624
|
const controller = new AbortController();
|
|
549
625
|
setState({
|
|
626
|
+
attributes: null,
|
|
550
627
|
experience: null,
|
|
551
|
-
loading: true
|
|
628
|
+
loading: true,
|
|
629
|
+
survey: null
|
|
552
630
|
});
|
|
553
631
|
client.bootstrap({
|
|
554
632
|
environment,
|
|
@@ -557,21 +635,25 @@ function useSunboardBootstrap(client, options) {
|
|
|
557
635
|
user: toRuntimeUser(user)
|
|
558
636
|
}).then((response) => {
|
|
559
637
|
if (controller.signal.aborted) return;
|
|
560
|
-
if (response.experiences.length === 0 && isDevEnvironment()) console.warn(`[sunboard] bootstrap returned no active experiences for route "${route}" in environment "${environment}". Check the dashboard for an enabled deployment whose trigger.route matches.`);
|
|
638
|
+
if (response.experiences.length === 0 && !response.survey && isDevEnvironment()) console.warn(`[sunboard] bootstrap returned no active experiences for route "${route}" in environment "${environment}". Check the dashboard for an enabled deployment whose trigger.route matches.`);
|
|
561
639
|
if (response.sdkAdvisory && isDevEnvironment() && !warnedSdkAdvisory.current) {
|
|
562
640
|
warnedSdkAdvisory.current = true;
|
|
563
641
|
console.warn(`[sunboard] ${response.sdkAdvisory.message}`);
|
|
564
642
|
}
|
|
565
643
|
setState({
|
|
644
|
+
attributes: response.attributes ?? null,
|
|
566
645
|
experience: response.experiences[0] ?? null,
|
|
567
|
-
loading: false
|
|
646
|
+
loading: false,
|
|
647
|
+
survey: response.survey ?? null
|
|
568
648
|
});
|
|
569
649
|
}).catch((error) => {
|
|
570
650
|
if (controller.signal.aborted) return;
|
|
571
651
|
if (isDevEnvironment()) console.warn("[sunboard] bootstrap failed", error);
|
|
572
652
|
setState({
|
|
653
|
+
attributes: null,
|
|
573
654
|
experience: null,
|
|
574
|
-
loading: false
|
|
655
|
+
loading: false,
|
|
656
|
+
survey: null
|
|
575
657
|
});
|
|
576
658
|
});
|
|
577
659
|
return () => {
|
|
@@ -581,6 +663,7 @@ function useSunboardBootstrap(client, options) {
|
|
|
581
663
|
client,
|
|
582
664
|
enabled,
|
|
583
665
|
environment,
|
|
666
|
+
nonce,
|
|
584
667
|
origin,
|
|
585
668
|
user?.id
|
|
586
669
|
]);
|
|
@@ -601,6 +684,10 @@ function isDevEnvironment() {
|
|
|
601
684
|
if (typeof process === "undefined" || !process.env) return false;
|
|
602
685
|
return process.env.NODE_ENV !== "production";
|
|
603
686
|
}
|
|
687
|
+
function isPreviewPage() {
|
|
688
|
+
if (typeof window === "undefined") return false;
|
|
689
|
+
return new URLSearchParams(window.location.search).get("sunboard_preview") === "1";
|
|
690
|
+
}
|
|
604
691
|
function toRuntimeUser(user) {
|
|
605
692
|
return {
|
|
606
693
|
email: user.email ?? void 0,
|
|
@@ -690,7 +777,7 @@ function actionLabelFor(step) {
|
|
|
690
777
|
if (step.action?.type === "startTour") return "Start";
|
|
691
778
|
if (step.action || step.route) return "Open";
|
|
692
779
|
}
|
|
693
|
-
function interpolateOptional(text, user) {
|
|
780
|
+
function interpolateOptional$1(text, user) {
|
|
694
781
|
if (!text) return void 0;
|
|
695
782
|
return interpolateTokens(text, user) || void 0;
|
|
696
783
|
}
|
|
@@ -712,17 +799,17 @@ function useChecklist() {
|
|
|
712
799
|
const sawIncompleteFirst = observationRef.current?.sawIncomplete ?? false;
|
|
713
800
|
const dismissed = dismissedKey !== null && dismissedKey === observationKey;
|
|
714
801
|
const rawSteps = useMemo(() => spec?.checklist?.steps ?? [], [spec]);
|
|
715
|
-
const title = useMemo(() => interpolateOptional(spec?.title, user ?? null), [spec?.title, user]);
|
|
716
|
-
const subtitle = useMemo(() => interpolateOptional(spec?.subtitle, user ?? null), [spec?.subtitle, user]);
|
|
802
|
+
const title = useMemo(() => interpolateOptional$1(spec?.title, user ?? null), [spec?.title, user]);
|
|
803
|
+
const subtitle = useMemo(() => interpolateOptional$1(spec?.subtitle, user ?? null), [spec?.subtitle, user]);
|
|
717
804
|
const completion = useMemo(() => {
|
|
718
805
|
const raw = spec?.completion;
|
|
719
806
|
if (!raw) return void 0;
|
|
720
|
-
const interpolatedTitle = interpolateOptional(raw.title, user ?? null);
|
|
807
|
+
const interpolatedTitle = interpolateOptional$1(raw.title, user ?? null);
|
|
721
808
|
if (!interpolatedTitle) return void 0;
|
|
722
809
|
return {
|
|
723
810
|
title: interpolatedTitle,
|
|
724
|
-
subtitle: interpolateOptional(raw.subtitle, user ?? null),
|
|
725
|
-
message: interpolateOptional(raw.message, user ?? null),
|
|
811
|
+
subtitle: interpolateOptional$1(raw.subtitle, user ?? null),
|
|
812
|
+
message: interpolateOptional$1(raw.message, user ?? null),
|
|
726
813
|
dismissAfterMs: raw.dismissAfterMs
|
|
727
814
|
};
|
|
728
815
|
}, [spec?.completion, user]);
|
|
@@ -768,6 +855,140 @@ function useChecklist() {
|
|
|
768
855
|
};
|
|
769
856
|
}
|
|
770
857
|
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/hooks/useSurvey.ts
|
|
860
|
+
function interpolateOptional(text, user) {
|
|
861
|
+
if (!text) return void 0;
|
|
862
|
+
return interpolateTokens(text, user) || void 0;
|
|
863
|
+
}
|
|
864
|
+
function answered(value) {
|
|
865
|
+
if (value === void 0 || value === null) return false;
|
|
866
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
867
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
function useSurvey() {
|
|
871
|
+
const { spec, user, loading, submitSurvey } = useSunboard();
|
|
872
|
+
const [answers, setAnswers] = useState({});
|
|
873
|
+
const [submitting, setSubmitting] = useState(false);
|
|
874
|
+
const [submitted, setSubmitted] = useState(false);
|
|
875
|
+
const [error, setError] = useState(null);
|
|
876
|
+
const [stepIndex, setStepIndex] = useState(0);
|
|
877
|
+
const isSurvey = Boolean(spec && spec.type === "survey" && spec.survey);
|
|
878
|
+
const ready = !loading && !!user && isSurvey;
|
|
879
|
+
const title = useMemo(() => interpolateOptional(spec?.title, user ?? null), [spec?.title, user]);
|
|
880
|
+
const subtitle = useMemo(() => interpolateOptional(spec?.subtitle, user ?? null), [spec?.subtitle, user]);
|
|
881
|
+
const steps = useMemo(() => {
|
|
882
|
+
return (spec?.survey?.steps ?? []).map((step) => ({
|
|
883
|
+
title: interpolateOptional(step.title, user ?? null),
|
|
884
|
+
subtitle: interpolateOptional(step.subtitle, user ?? null),
|
|
885
|
+
questions: step.questions.map((question) => ({
|
|
886
|
+
key: question.key,
|
|
887
|
+
prompt: interpolateTokens(question.prompt, user ?? null),
|
|
888
|
+
type: question.type,
|
|
889
|
+
required: question.required,
|
|
890
|
+
options: (question.options ?? []).map((option) => ({
|
|
891
|
+
value: option.value,
|
|
892
|
+
label: interpolateTokens(option.label, user ?? null)
|
|
893
|
+
}))
|
|
894
|
+
}))
|
|
895
|
+
}));
|
|
896
|
+
}, [spec?.survey, user]);
|
|
897
|
+
const questions = useMemo(() => steps.flatMap((step) => step.questions), [steps]);
|
|
898
|
+
const signature = questions.map((question) => question.key).join("|");
|
|
899
|
+
const signatureRef = useRef(signature);
|
|
900
|
+
if (signatureRef.current !== signature) {
|
|
901
|
+
signatureRef.current = signature;
|
|
902
|
+
if (stepIndex !== 0) setStepIndex(0);
|
|
903
|
+
}
|
|
904
|
+
const totalSteps = steps.length;
|
|
905
|
+
const safeIndex = totalSteps > 0 ? Math.min(stepIndex, totalSteps - 1) : 0;
|
|
906
|
+
const currentStep = steps[safeIndex];
|
|
907
|
+
const setAnswer = useCallback((key, value) => {
|
|
908
|
+
setAnswers((current) => ({
|
|
909
|
+
...current,
|
|
910
|
+
[key]: value
|
|
911
|
+
}));
|
|
912
|
+
}, []);
|
|
913
|
+
const toggleAnswer = useCallback((key, value) => {
|
|
914
|
+
setAnswers((current) => {
|
|
915
|
+
const existing = Array.isArray(current[key]) ? current[key] : [];
|
|
916
|
+
const next$1 = existing.includes(value) ? existing.filter((item) => item !== value) : [...existing, value];
|
|
917
|
+
return {
|
|
918
|
+
...current,
|
|
919
|
+
[key]: next$1
|
|
920
|
+
};
|
|
921
|
+
});
|
|
922
|
+
}, []);
|
|
923
|
+
const isAnswered = useCallback((key) => answered(answers[key]), [answers]);
|
|
924
|
+
const canSubmit = useMemo(() => {
|
|
925
|
+
if (!ready || submitting) return false;
|
|
926
|
+
return questions.filter((question) => question.required).every((question) => answered(answers[question.key]));
|
|
927
|
+
}, [
|
|
928
|
+
answers,
|
|
929
|
+
questions,
|
|
930
|
+
ready,
|
|
931
|
+
submitting
|
|
932
|
+
]);
|
|
933
|
+
const canAdvance = useMemo(() => {
|
|
934
|
+
if (!currentStep) return false;
|
|
935
|
+
return currentStep.questions.filter((question) => question.required).every((question) => answered(answers[question.key]));
|
|
936
|
+
}, [answers, currentStep]);
|
|
937
|
+
const submit = useCallback(async () => {
|
|
938
|
+
if (!canSubmit) return;
|
|
939
|
+
setSubmitting(true);
|
|
940
|
+
setError(null);
|
|
941
|
+
const result = await submitSurvey(answers);
|
|
942
|
+
setSubmitting(false);
|
|
943
|
+
if (result.ok) {
|
|
944
|
+
setSubmitted(true);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
setError(result.reason === "network-error" ? "Something went wrong. Please try again." : "Survey is not ready.");
|
|
948
|
+
}, [
|
|
949
|
+
answers,
|
|
950
|
+
canSubmit,
|
|
951
|
+
submitSurvey
|
|
952
|
+
]);
|
|
953
|
+
const isLastStep = totalSteps === 0 || safeIndex >= totalSteps - 1;
|
|
954
|
+
const isFirstStep = safeIndex === 0;
|
|
955
|
+
const next = useCallback(() => {
|
|
956
|
+
if (isLastStep) submit();
|
|
957
|
+
else setStepIndex(safeIndex + 1);
|
|
958
|
+
}, [
|
|
959
|
+
isLastStep,
|
|
960
|
+
safeIndex,
|
|
961
|
+
submit
|
|
962
|
+
]);
|
|
963
|
+
const back = useCallback(() => {
|
|
964
|
+
setStepIndex((current) => Math.max(0, current - 1));
|
|
965
|
+
}, []);
|
|
966
|
+
return {
|
|
967
|
+
visible: ready && !submitted,
|
|
968
|
+
title,
|
|
969
|
+
subtitle,
|
|
970
|
+
questions,
|
|
971
|
+
steps,
|
|
972
|
+
stepIndex: safeIndex,
|
|
973
|
+
totalSteps,
|
|
974
|
+
currentStep,
|
|
975
|
+
progress: totalSteps > 0 ? (safeIndex + 1) / totalSteps * 100 : 0,
|
|
976
|
+
isFirstStep,
|
|
977
|
+
isLastStep,
|
|
978
|
+
canAdvance,
|
|
979
|
+
next,
|
|
980
|
+
back,
|
|
981
|
+
answers,
|
|
982
|
+
setAnswer,
|
|
983
|
+
toggleAnswer,
|
|
984
|
+
isAnswered,
|
|
985
|
+
canSubmit,
|
|
986
|
+
submitting,
|
|
987
|
+
error,
|
|
988
|
+
submit
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
771
992
|
//#endregion
|
|
772
993
|
//#region src/icons/index.tsx
|
|
773
994
|
const CheckIcon = () => {
|
|
@@ -1254,12 +1475,130 @@ function SunboardChecklist() {
|
|
|
1254
1475
|
]
|
|
1255
1476
|
});
|
|
1256
1477
|
}
|
|
1478
|
+
function SunboardSurvey() {
|
|
1479
|
+
const { visible, title, subtitle, currentStep, stepIndex, totalSteps, progress, isFirstStep, isLastStep, canAdvance, canSubmit, next, back, answers, setAnswer, toggleAnswer, submitting, error } = useSurvey();
|
|
1480
|
+
if (!visible || !title || !currentStep) return null;
|
|
1481
|
+
const showProgress = totalSteps > 1;
|
|
1482
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1483
|
+
className: "sunboard-survey-overlay",
|
|
1484
|
+
children: /* @__PURE__ */ jsxs("section", {
|
|
1485
|
+
className: "sunboard-survey-card",
|
|
1486
|
+
role: "dialog",
|
|
1487
|
+
"aria-modal": "true",
|
|
1488
|
+
"aria-label": `${title} survey`,
|
|
1489
|
+
children: [
|
|
1490
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1491
|
+
className: "sunboard-survey-head",
|
|
1492
|
+
children: [subtitle ? /* @__PURE__ */ jsx("p", {
|
|
1493
|
+
className: "sunboard-eyebrow",
|
|
1494
|
+
children: subtitle
|
|
1495
|
+
}) : null, /* @__PURE__ */ jsx("h2", {
|
|
1496
|
+
className: "sunboard-title",
|
|
1497
|
+
children: title
|
|
1498
|
+
})]
|
|
1499
|
+
}),
|
|
1500
|
+
showProgress ? /* @__PURE__ */ jsxs("div", {
|
|
1501
|
+
className: "sunboard-survey-progress-row",
|
|
1502
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
1503
|
+
className: "sunboard-survey-stepcount",
|
|
1504
|
+
children: [
|
|
1505
|
+
"Step ",
|
|
1506
|
+
stepIndex + 1,
|
|
1507
|
+
" of ",
|
|
1508
|
+
totalSteps
|
|
1509
|
+
]
|
|
1510
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1511
|
+
className: "sunboard-progress",
|
|
1512
|
+
role: "progressbar",
|
|
1513
|
+
"aria-label": `${title} progress`,
|
|
1514
|
+
"aria-valuemin": 1,
|
|
1515
|
+
"aria-valuemax": totalSteps,
|
|
1516
|
+
"aria-valuenow": stepIndex + 1,
|
|
1517
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1518
|
+
className: "sunboard-progress-fill",
|
|
1519
|
+
style: { width: `${progress}%` }
|
|
1520
|
+
})
|
|
1521
|
+
})]
|
|
1522
|
+
}) : null,
|
|
1523
|
+
/* @__PURE__ */ jsxs("form", {
|
|
1524
|
+
className: "sunboard-survey-form",
|
|
1525
|
+
onSubmit: (event) => {
|
|
1526
|
+
event.preventDefault();
|
|
1527
|
+
next();
|
|
1528
|
+
},
|
|
1529
|
+
children: [
|
|
1530
|
+
currentStep.title ? /* @__PURE__ */ jsx("p", {
|
|
1531
|
+
className: "sunboard-survey-step-title",
|
|
1532
|
+
children: currentStep.title
|
|
1533
|
+
}) : null,
|
|
1534
|
+
/* @__PURE__ */ jsx("ol", {
|
|
1535
|
+
className: "sunboard-survey-questions",
|
|
1536
|
+
children: currentStep.questions.map((question) => /* @__PURE__ */ jsxs("li", {
|
|
1537
|
+
className: "sunboard-survey-question",
|
|
1538
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
1539
|
+
className: "sunboard-survey-prompt",
|
|
1540
|
+
children: [question.prompt, question.required ? /* @__PURE__ */ jsx("span", {
|
|
1541
|
+
"aria-hidden": "true",
|
|
1542
|
+
className: "sunboard-survey-required",
|
|
1543
|
+
children: " *"
|
|
1544
|
+
}) : null]
|
|
1545
|
+
}), question.type === "text" ? /* @__PURE__ */ jsx("input", {
|
|
1546
|
+
className: "sunboard-survey-input",
|
|
1547
|
+
type: "text",
|
|
1548
|
+
value: typeof answers[question.key] === "string" ? answers[question.key] : "",
|
|
1549
|
+
required: question.required,
|
|
1550
|
+
"aria-label": question.prompt,
|
|
1551
|
+
onChange: (event) => setAnswer(question.key, event.target.value)
|
|
1552
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1553
|
+
className: "sunboard-survey-options",
|
|
1554
|
+
role: "group",
|
|
1555
|
+
"aria-label": question.prompt,
|
|
1556
|
+
children: question.options.map((option) => {
|
|
1557
|
+
const selected = question.type === "multi-select" ? Array.isArray(answers[question.key]) && answers[question.key].includes(option.value) : answers[question.key] === option.value;
|
|
1558
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1559
|
+
className: "sunboard-survey-option",
|
|
1560
|
+
type: "button",
|
|
1561
|
+
"aria-pressed": selected,
|
|
1562
|
+
"data-selected": selected ? "true" : "false",
|
|
1563
|
+
onClick: () => question.type === "multi-select" ? toggleAnswer(question.key, option.value) : setAnswer(question.key, option.value),
|
|
1564
|
+
children: option.label
|
|
1565
|
+
}, option.value);
|
|
1566
|
+
})
|
|
1567
|
+
})]
|
|
1568
|
+
}, question.key))
|
|
1569
|
+
}),
|
|
1570
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
1571
|
+
className: "sunboard-survey-error",
|
|
1572
|
+
role: "alert",
|
|
1573
|
+
children: error
|
|
1574
|
+
}) : null,
|
|
1575
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1576
|
+
className: "sunboard-survey-nav",
|
|
1577
|
+
children: [!isFirstStep ? /* @__PURE__ */ jsx("button", {
|
|
1578
|
+
className: "sunboard-survey-back",
|
|
1579
|
+
type: "button",
|
|
1580
|
+
onClick: back,
|
|
1581
|
+
children: "Back"
|
|
1582
|
+
}) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsx("button", {
|
|
1583
|
+
className: "sunboard-survey-submit",
|
|
1584
|
+
type: "submit",
|
|
1585
|
+
disabled: isLastStep ? !canSubmit : !canAdvance,
|
|
1586
|
+
children: isLastStep ? submitting ? "Submitting…" : "Finish" : "Continue"
|
|
1587
|
+
})]
|
|
1588
|
+
})
|
|
1589
|
+
]
|
|
1590
|
+
})
|
|
1591
|
+
]
|
|
1592
|
+
})
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1257
1595
|
const Sunboard = Object.assign(SunboardChecklist, {
|
|
1258
1596
|
Checklist: SunboardChecklist,
|
|
1597
|
+
Survey: SunboardSurvey,
|
|
1259
1598
|
Hotspots: SunboardHotspots,
|
|
1260
1599
|
Tour: SunboardTour
|
|
1261
1600
|
});
|
|
1262
1601
|
|
|
1263
1602
|
//#endregion
|
|
1264
|
-
export { Sunboard, SunboardChecklist, SunboardHotspots, SunboardProvider, SunboardRuntimeApiError, SunboardRuntimeClient, SunboardTour, useChecklist, useSunboard, useSunboardPreviewSpec };
|
|
1603
|
+
export { Sunboard, SunboardChecklist, SunboardHotspots, SunboardProvider, SunboardRuntimeApiError, SunboardRuntimeClient, SunboardSurvey, SunboardTour, useChecklist, useSunboard, useSunboardPreviewSpec, useSurvey };
|
|
1265
1604
|
//# sourceMappingURL=index.mjs.map
|