@sunboard/react 0.1.2 → 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.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
- return typeof candidate.key === "string" && typeof candidate.name === "string" && typeof candidate.type === "string" && typeof candidate.checklist === "object" && Array.isArray(candidate.checklist?.steps);
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 hostedSpec = activeExperience?.spec ?? null;
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.steps.filter((step) => step.completeWhen?.tour && effectiveCompletedTourKeys.has(step.completeWhen.tour)).map((step) => step.key);
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.steps.length ?? 0;
274
- const completedCount = resolvedSpec?.checklist.steps.filter((step) => effectiveCompletedStepKeys.has(step.key)).length ?? 0;
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.steps.filter((step) => step.completeWhen?.event === eventName).map((step) => step.key);
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);
@@ -377,12 +447,35 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
377
447
  user,
378
448
  versionId
379
449
  ]);
380
- const dismissTour = useCallback(async () => {
450
+ const dismissTour = useCallback(async (options) => {
451
+ const dismissed = activeTourState;
452
+ const dismissedStepKey = dismissed ? resolvedSpec?.tours[dismissed.key]?.steps[dismissed.stepIndex]?.key : void 0;
381
453
  setActiveTourState(null);
382
454
  setTourError(null);
383
455
  lastNavigatedTourStep.current = null;
456
+ if (!options?.silent && hostedMode && client && experienceKey && user && dismissed && dismissedStepKey) client.dismissTour({
457
+ environment,
458
+ experienceKey,
459
+ origin: currentOrigin,
460
+ route: currentRoute,
461
+ stepKey: dismissedStepKey,
462
+ tourKey: dismissed.key,
463
+ user: toRuntimeUser(user),
464
+ versionId: versionId ?? void 0
465
+ }).catch(() => {});
384
466
  return { ok: true };
385
- }, []);
467
+ }, [
468
+ activeTourState,
469
+ client,
470
+ currentOrigin,
471
+ currentRoute,
472
+ environment,
473
+ experienceKey,
474
+ hostedMode,
475
+ resolvedSpec,
476
+ user,
477
+ versionId
478
+ ]);
386
479
  const finishActiveTour = useCallback(async (tourKey) => {
387
480
  completeTour(tourKey);
388
481
  if (!hostedMode || !client || !experienceKey || !user) return;
@@ -463,7 +556,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
463
556
  }, []);
464
557
  const value = useMemo(() => ({
465
558
  spec: resolvedSpec,
466
- user,
559
+ user: effectiveUser,
467
560
  loading: resolvedLoading,
468
561
  mode: resolvedMode,
469
562
  route: currentRoute,
@@ -474,6 +567,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
474
567
  activeTour,
475
568
  tourError,
476
569
  track,
570
+ submitSurvey,
477
571
  startTour,
478
572
  dismissTour,
479
573
  advanceTour,
@@ -483,7 +577,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
483
577
  visibleHotspots
484
578
  }), [
485
579
  resolvedSpec,
486
- user,
580
+ effectiveUser,
487
581
  resolvedLoading,
488
582
  resolvedMode,
489
583
  currentRoute,
@@ -494,6 +588,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
494
588
  activeTour,
495
589
  tourError,
496
590
  track,
591
+ submitSurvey,
497
592
  startTour,
498
593
  dismissTour,
499
594
  advanceTour,
@@ -508,24 +603,30 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
508
603
  });
509
604
  }
510
605
  function useSunboardBootstrap(client, options) {
511
- const { enabled, environment, origin, route, user } = options;
606
+ const { enabled, environment, nonce, origin, route, user } = options;
512
607
  const [state, setState] = useState({
608
+ attributes: null,
513
609
  experience: null,
514
- loading: false
610
+ loading: false,
611
+ survey: null
515
612
  });
516
613
  const warnedSdkAdvisory = useRef(false);
517
614
  useEffect(() => {
518
- if (!client || !enabled || !user?.id) {
615
+ if (!client || !enabled || !user?.id || isPreviewPage()) {
519
616
  setState({
617
+ attributes: null,
520
618
  experience: null,
521
- loading: false
619
+ loading: false,
620
+ survey: null
522
621
  });
523
622
  return;
524
623
  }
525
624
  const controller = new AbortController();
526
625
  setState({
626
+ attributes: null,
527
627
  experience: null,
528
- loading: true
628
+ loading: true,
629
+ survey: null
529
630
  });
530
631
  client.bootstrap({
531
632
  environment,
@@ -534,21 +635,25 @@ function useSunboardBootstrap(client, options) {
534
635
  user: toRuntimeUser(user)
535
636
  }).then((response) => {
536
637
  if (controller.signal.aborted) return;
537
- 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.`);
538
639
  if (response.sdkAdvisory && isDevEnvironment() && !warnedSdkAdvisory.current) {
539
640
  warnedSdkAdvisory.current = true;
540
641
  console.warn(`[sunboard] ${response.sdkAdvisory.message}`);
541
642
  }
542
643
  setState({
644
+ attributes: response.attributes ?? null,
543
645
  experience: response.experiences[0] ?? null,
544
- loading: false
646
+ loading: false,
647
+ survey: response.survey ?? null
545
648
  });
546
649
  }).catch((error) => {
547
650
  if (controller.signal.aborted) return;
548
651
  if (isDevEnvironment()) console.warn("[sunboard] bootstrap failed", error);
549
652
  setState({
653
+ attributes: null,
550
654
  experience: null,
551
- loading: false
655
+ loading: false,
656
+ survey: null
552
657
  });
553
658
  });
554
659
  return () => {
@@ -558,6 +663,7 @@ function useSunboardBootstrap(client, options) {
558
663
  client,
559
664
  enabled,
560
665
  environment,
666
+ nonce,
561
667
  origin,
562
668
  user?.id
563
669
  ]);
@@ -578,6 +684,10 @@ function isDevEnvironment() {
578
684
  if (typeof process === "undefined" || !process.env) return false;
579
685
  return process.env.NODE_ENV !== "production";
580
686
  }
687
+ function isPreviewPage() {
688
+ if (typeof window === "undefined") return false;
689
+ return new URLSearchParams(window.location.search).get("sunboard_preview") === "1";
690
+ }
581
691
  function toRuntimeUser(user) {
582
692
  return {
583
693
  email: user.email ?? void 0,
@@ -667,7 +777,7 @@ function actionLabelFor(step) {
667
777
  if (step.action?.type === "startTour") return "Start";
668
778
  if (step.action || step.route) return "Open";
669
779
  }
670
- function interpolateOptional(text, user) {
780
+ function interpolateOptional$1(text, user) {
671
781
  if (!text) return void 0;
672
782
  return interpolateTokens(text, user) || void 0;
673
783
  }
@@ -689,17 +799,17 @@ function useChecklist() {
689
799
  const sawIncompleteFirst = observationRef.current?.sawIncomplete ?? false;
690
800
  const dismissed = dismissedKey !== null && dismissedKey === observationKey;
691
801
  const rawSteps = useMemo(() => spec?.checklist?.steps ?? [], [spec]);
692
- const title = useMemo(() => interpolateOptional(spec?.title, user ?? null), [spec?.title, user]);
693
- 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]);
694
804
  const completion = useMemo(() => {
695
805
  const raw = spec?.completion;
696
806
  if (!raw) return void 0;
697
- const interpolatedTitle = interpolateOptional(raw.title, user ?? null);
807
+ const interpolatedTitle = interpolateOptional$1(raw.title, user ?? null);
698
808
  if (!interpolatedTitle) return void 0;
699
809
  return {
700
810
  title: interpolatedTitle,
701
- subtitle: interpolateOptional(raw.subtitle, user ?? null),
702
- message: interpolateOptional(raw.message, user ?? null),
811
+ subtitle: interpolateOptional$1(raw.subtitle, user ?? null),
812
+ message: interpolateOptional$1(raw.message, user ?? null),
703
813
  dismissAfterMs: raw.dismissAfterMs
704
814
  };
705
815
  }, [spec?.completion, user]);
@@ -745,6 +855,140 @@ function useChecklist() {
745
855
  };
746
856
  }
747
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
+
748
992
  //#endregion
749
993
  //#region src/icons/index.tsx
750
994
  const CheckIcon = () => {
@@ -942,7 +1186,7 @@ function SunboardTour() {
942
1186
  return;
943
1187
  }
944
1188
  reportTourError(null);
945
- dismissTour();
1189
+ dismissTour({ silent: true });
946
1190
  }, [
947
1191
  activeTour,
948
1192
  dismissTour,
@@ -1231,12 +1475,130 @@ function SunboardChecklist() {
1231
1475
  ]
1232
1476
  });
1233
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
+ }
1234
1595
  const Sunboard = Object.assign(SunboardChecklist, {
1235
1596
  Checklist: SunboardChecklist,
1597
+ Survey: SunboardSurvey,
1236
1598
  Hotspots: SunboardHotspots,
1237
1599
  Tour: SunboardTour
1238
1600
  });
1239
1601
 
1240
1602
  //#endregion
1241
- 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 };
1242
1604
  //# sourceMappingURL=index.mjs.map