@sunboard/react 0.1.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 ADDED
@@ -0,0 +1,1250 @@
1
+ "use client";
2
+
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ import { SunboardRuntimeApiError, SunboardRuntimeClient, SunboardRuntimeClient as SunboardRuntimeClient$1, routeMatches } from "@sunboard/core/runtime-api";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { FloatingArrow, FloatingPortal, arrow, autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react";
7
+
8
+ //#region src/adapters/navigation.ts
9
+ async function navigateWithFallback(url, navigate) {
10
+ if (navigate) {
11
+ await navigate(url);
12
+ return;
13
+ }
14
+ if (typeof window !== "undefined") window.location.assign(url);
15
+ }
16
+
17
+ //#endregion
18
+ //#region src/hooks/sunboard-context.ts
19
+ const SunboardContext = createContext(null);
20
+
21
+ //#endregion
22
+ //#region src/hooks/useSunboardPreviewSpec.ts
23
+ function useSunboardPreviewSpec() {
24
+ const [state, setState] = useState(() => ({
25
+ error: null,
26
+ loading: getPreviewSpecUrl() !== null,
27
+ source: "none",
28
+ spec: null
29
+ }));
30
+ useEffect(() => {
31
+ const previewUrl = getPreviewSpecUrl();
32
+ if (!previewUrl) {
33
+ setState({
34
+ error: null,
35
+ loading: false,
36
+ source: "none",
37
+ spec: null
38
+ });
39
+ return;
40
+ }
41
+ const controller = new AbortController();
42
+ let cancelled = false;
43
+ let eventSource = null;
44
+ const loadSpec = async (signal) => {
45
+ const response = await fetch(previewUrl.toString(), { signal });
46
+ if (!response.ok) throw new Error(`Sunboard preview failed with ${response.status}.`);
47
+ const json = await response.json();
48
+ if (!isSunboardSpecLike(json)) throw new Error("Sunboard preview returned an invalid spec payload.");
49
+ return json;
50
+ };
51
+ setState({
52
+ error: null,
53
+ loading: true,
54
+ source: "none",
55
+ spec: null
56
+ });
57
+ loadSpec(controller.signal).then((previewSpec) => {
58
+ if (cancelled) return;
59
+ setState({
60
+ error: null,
61
+ loading: false,
62
+ source: "preview",
63
+ spec: previewSpec
64
+ });
65
+ const eventsUrl = buildEventsUrl(previewUrl);
66
+ if (eventsUrl && typeof EventSource !== "undefined") {
67
+ eventSource = new EventSource(eventsUrl);
68
+ let firstMessage = true;
69
+ eventSource.onmessage = () => {
70
+ if (firstMessage) {
71
+ firstMessage = false;
72
+ return;
73
+ }
74
+ reloadSpec(previewUrl);
75
+ };
76
+ eventSource.onerror = () => {};
77
+ }
78
+ }).catch((error) => {
79
+ if (controller.signal.aborted || cancelled) return;
80
+ setState({
81
+ error: error instanceof Error ? error : new Error(String(error)),
82
+ loading: false,
83
+ source: "none",
84
+ spec: null
85
+ });
86
+ });
87
+ const reloadSpec = async (url) => {
88
+ try {
89
+ const response = await fetch(url.toString());
90
+ if (!response.ok) throw new Error(`Sunboard preview failed with ${response.status}.`);
91
+ const json = await response.json();
92
+ if (!isSunboardSpecLike(json)) throw new Error("Sunboard preview returned an invalid spec payload.");
93
+ if (cancelled) return;
94
+ setState({
95
+ error: null,
96
+ loading: false,
97
+ source: "preview",
98
+ spec: json
99
+ });
100
+ } catch (error) {
101
+ if (cancelled) return;
102
+ setState((current) => ({
103
+ ...current,
104
+ error: error instanceof Error ? error : new Error(String(error))
105
+ }));
106
+ }
107
+ };
108
+ return () => {
109
+ cancelled = true;
110
+ controller.abort();
111
+ eventSource?.close();
112
+ };
113
+ }, []);
114
+ return state;
115
+ }
116
+ function buildEventsUrl(specUrl) {
117
+ try {
118
+ const eventsUrl = new URL(specUrl.toString());
119
+ eventsUrl.pathname = eventsUrl.pathname.replace(/\/spec\.json$/, "/events");
120
+ if (eventsUrl.pathname === specUrl.pathname) return null;
121
+ return eventsUrl.toString();
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+ function getPreviewSpecUrl() {
127
+ if (typeof window === "undefined") return null;
128
+ const pageUrl = new URL(window.location.href);
129
+ if (pageUrl.searchParams.get("sunboard_preview") !== "1") return null;
130
+ const previewUrlParam = pageUrl.searchParams.get("sunboard_preview_url");
131
+ if (!previewUrlParam) return null;
132
+ try {
133
+ const previewUrl = new URL(previewUrlParam);
134
+ return isAllowedLocalPreviewUrl(previewUrl) ? previewUrl : null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ function isAllowedLocalPreviewUrl(url) {
140
+ return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
141
+ }
142
+ function isSunboardSpecLike(value) {
143
+ if (!value || typeof value !== "object") return false;
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);
146
+ }
147
+
148
+ //#endregion
149
+ //#region src/version.ts
150
+ const SDK_VERSION = "0.0.0";
151
+
152
+ //#endregion
153
+ //#region src/components/SunboardProvider.tsx
154
+ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: modeProp, route, navigate, apiUrl, publishableKey, environment = "production", onTrack, children }) {
155
+ const currentRoute = route ?? getBrowserRoute();
156
+ const currentOrigin = getBrowserOrigin();
157
+ const preview = useSunboardPreviewSpec();
158
+ const client = useMemo(() => {
159
+ if (!apiUrl || !publishableKey) return null;
160
+ return new SunboardRuntimeClient$1({
161
+ apiUrl,
162
+ publishableKey,
163
+ sdkVersion: SDK_VERSION
164
+ });
165
+ }, [apiUrl, publishableKey]);
166
+ const bootstrap = useSunboardBootstrap(client, {
167
+ enabled: !specProp && preview.source !== "preview" && !preview.loading,
168
+ environment,
169
+ origin: currentOrigin,
170
+ route: currentRoute,
171
+ user
172
+ });
173
+ const activeExperience = bootstrap.experience;
174
+ const hostedSpec = activeExperience?.spec ?? null;
175
+ const resolvedSpec = specProp ?? preview.spec ?? hostedSpec;
176
+ const resolvedLoading = loadingProp ?? (preview.loading || bootstrap.loading);
177
+ const resolvedMode = modeProp ?? (preview.source === "preview" ? "preview" : "production");
178
+ const hostedMode = client !== null && !specProp && preview.source !== "preview";
179
+ const [completedStepKeys, setCompletedStepKeys] = useState(() => /* @__PURE__ */ new Set());
180
+ const [completedTourKeys, setCompletedTourKeys] = useState(() => /* @__PURE__ */ new Set());
181
+ const [activeTourState, setActiveTourState] = useState(null);
182
+ const [tourError, setTourError] = useState(null);
183
+ const lastNavigatedTourStep = useRef(null);
184
+ 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;
187
+ useEffect(() => {
188
+ setCompletedStepKeys(/* @__PURE__ */ new Set());
189
+ setActiveTourState(null);
190
+ setTourError(null);
191
+ lastNavigatedTourStep.current = null;
192
+ if (!storageKey) {
193
+ setCompletedTourKeys(/* @__PURE__ */ new Set());
194
+ return;
195
+ }
196
+ setCompletedTourKeys(readCompletedTours(storageKey));
197
+ }, [
198
+ resolvedSpec?.key,
199
+ storageKey,
200
+ user?.id
201
+ ]);
202
+ useEffect(() => {
203
+ if (!hostedMode || !activeExperience) return;
204
+ applyProgress(activeExperience.progress, setCompletedStepKeys, setCompletedTourKeys);
205
+ }, [activeExperience, hostedMode]);
206
+ const effectiveCompletedStepKeys = useMemo(() => {
207
+ if (!hostedMode || !activeExperience) return completedStepKeys;
208
+ return mergeKeys(completedStepKeys, activeExperience.progress.completedStepKeys);
209
+ }, [
210
+ activeExperience,
211
+ completedStepKeys,
212
+ hostedMode
213
+ ]);
214
+ const effectiveCompletedTourKeys = useMemo(() => {
215
+ if (!hostedMode || !activeExperience) return completedTourKeys;
216
+ return mergeKeys(completedTourKeys, activeExperience.progress.completedTourKeys);
217
+ }, [
218
+ activeExperience,
219
+ completedTourKeys,
220
+ hostedMode
221
+ ]);
222
+ useEffect(() => {
223
+ if (!resolvedSpec) return;
224
+ const tourCompletedStepKeys = resolvedSpec.checklist.steps.filter((step) => step.completeWhen?.tour && effectiveCompletedTourKeys.has(step.completeWhen.tour)).map((step) => step.key);
225
+ if (tourCompletedStepKeys.length === 0) return;
226
+ setCompletedStepKeys((current) => {
227
+ const next = new Set(current);
228
+ for (const stepKey of tourCompletedStepKeys) next.add(stepKey);
229
+ return next;
230
+ });
231
+ }, [effectiveCompletedTourKeys, resolvedSpec]);
232
+ const activeTour = useMemo(() => {
233
+ if (!resolvedSpec || !activeTourState) return null;
234
+ const tour = resolvedSpec.tours[activeTourState.key];
235
+ const step = tour?.steps[activeTourState.stepIndex];
236
+ if (!tour || !step) return null;
237
+ return {
238
+ key: activeTourState.key,
239
+ name: tour.name,
240
+ step,
241
+ stepIndex: activeTourState.stepIndex,
242
+ totalSteps: tour.steps.length
243
+ };
244
+ }, [activeTourState, resolvedSpec]);
245
+ useEffect(() => {
246
+ if (!activeTour?.step.route || routeMatches(activeTour.step.route, currentRoute)) return;
247
+ const navigationKey = `${activeTour.key}:${activeTour.step.key}:${activeTour.step.route}`;
248
+ if (lastNavigatedTourStep.current === navigationKey) return;
249
+ lastNavigatedTourStep.current = navigationKey;
250
+ navigateWithFallback(activeTour.step.route, navigate);
251
+ }, [
252
+ activeTour,
253
+ currentRoute,
254
+ navigate
255
+ ]);
256
+ useEffect(() => {
257
+ if (!hostedMode || !client || !activeTour || !user || !experienceKey) return;
258
+ client.recordTourStep({
259
+ environment,
260
+ experienceKey,
261
+ origin: currentOrigin,
262
+ route: currentRoute,
263
+ stepKey: activeTour.step.key,
264
+ tourKey: activeTour.key,
265
+ user: toRuntimeUser(user),
266
+ versionId: versionId ?? void 0
267
+ }).catch(() => {});
268
+ }, [
269
+ activeTour?.key,
270
+ activeTour?.step.key,
271
+ client,
272
+ currentOrigin,
273
+ experienceKey,
274
+ hostedMode,
275
+ user?.id
276
+ ]);
277
+ const totalSteps = resolvedSpec?.checklist.steps.length ?? 0;
278
+ const completedCount = resolvedSpec?.checklist.steps.filter((step) => effectiveCompletedStepKeys.has(step.key)).length ?? 0;
279
+ const completeTour = useCallback((tourKey) => {
280
+ setCompletedTourKeys((current) => {
281
+ const next = new Set(current);
282
+ next.add(tourKey);
283
+ if (storageKey) writeCompletedTours(storageKey, next);
284
+ return next;
285
+ });
286
+ }, [storageKey]);
287
+ const track = useCallback(async (eventName, properties) => {
288
+ if (resolvedLoading || !resolvedSpec) return {
289
+ ok: false,
290
+ reason: "not-ready"
291
+ };
292
+ if (!user) return {
293
+ ok: false,
294
+ reason: "missing-user"
295
+ };
296
+ const matchingStepKeys = resolvedSpec.checklist.steps.filter((step) => step.completeWhen?.event === eventName).map((step) => step.key);
297
+ if (matchingStepKeys.length > 0) setCompletedStepKeys((current) => {
298
+ const next = new Set(current);
299
+ for (const stepKey of matchingStepKeys) next.add(stepKey);
300
+ return next;
301
+ });
302
+ let networkOk = true;
303
+ if (hostedMode && client && experienceKey) try {
304
+ applyProgress((await client.track({
305
+ environment,
306
+ eventName,
307
+ experienceKey,
308
+ origin: currentOrigin,
309
+ properties,
310
+ route: currentRoute,
311
+ user: toRuntimeUser(user),
312
+ versionId: versionId ?? void 0
313
+ })).progress, setCompletedStepKeys, setCompletedTourKeys);
314
+ } catch {
315
+ networkOk = false;
316
+ }
317
+ try {
318
+ await onTrack?.(eventName, properties);
319
+ } catch {
320
+ return {
321
+ ok: false,
322
+ reason: "network-error"
323
+ };
324
+ }
325
+ if (!networkOk) return {
326
+ ok: false,
327
+ reason: "network-error"
328
+ };
329
+ return { ok: true };
330
+ }, [
331
+ client,
332
+ currentRoute,
333
+ currentOrigin,
334
+ environment,
335
+ experienceKey,
336
+ hostedMode,
337
+ onTrack,
338
+ resolvedLoading,
339
+ resolvedSpec,
340
+ user,
341
+ versionId
342
+ ]);
343
+ const navigateToStep = useCallback(async (step) => {
344
+ if (!step.route) return;
345
+ await navigateWithFallback(step.route, navigate);
346
+ }, [navigate]);
347
+ const startTour = useCallback(async (tourKey) => {
348
+ if (resolvedLoading || !resolvedSpec) return {
349
+ ok: false,
350
+ reason: "not-ready"
351
+ };
352
+ if (!user) return {
353
+ ok: false,
354
+ reason: "missing-user"
355
+ };
356
+ if (!resolvedSpec.tours[tourKey]) return {
357
+ ok: false,
358
+ reason: "missing-tour"
359
+ };
360
+ setTourError(null);
361
+ lastNavigatedTourStep.current = null;
362
+ setActiveTourState({
363
+ key: tourKey,
364
+ stepIndex: 0
365
+ });
366
+ if (hostedMode && client && experienceKey && user) client.startTour({
367
+ environment,
368
+ experienceKey,
369
+ origin: currentOrigin,
370
+ route: currentRoute,
371
+ tourKey,
372
+ user: toRuntimeUser(user),
373
+ versionId: versionId ?? void 0
374
+ }).catch(() => {});
375
+ return { ok: true };
376
+ }, [
377
+ client,
378
+ currentRoute,
379
+ currentOrigin,
380
+ environment,
381
+ experienceKey,
382
+ hostedMode,
383
+ resolvedLoading,
384
+ resolvedSpec,
385
+ user,
386
+ versionId
387
+ ]);
388
+ const dismissTour = useCallback(async () => {
389
+ setActiveTourState(null);
390
+ setTourError(null);
391
+ lastNavigatedTourStep.current = null;
392
+ return { ok: true };
393
+ }, []);
394
+ const finishActiveTour = useCallback(async (tourKey) => {
395
+ completeTour(tourKey);
396
+ if (!hostedMode || !client || !experienceKey || !user) return;
397
+ try {
398
+ applyProgress((await client.completeTour({
399
+ environment,
400
+ experienceKey,
401
+ origin: currentOrigin,
402
+ route: currentRoute,
403
+ tourKey,
404
+ user: toRuntimeUser(user),
405
+ versionId: versionId ?? void 0
406
+ })).progress, setCompletedStepKeys, setCompletedTourKeys);
407
+ } catch {}
408
+ }, [
409
+ client,
410
+ completeTour,
411
+ currentRoute,
412
+ currentOrigin,
413
+ environment,
414
+ experienceKey,
415
+ hostedMode,
416
+ user,
417
+ versionId
418
+ ]);
419
+ const advanceTour = useCallback(async () => {
420
+ if (resolvedLoading || !resolvedSpec || !activeTourState) return {
421
+ ok: false,
422
+ reason: "not-ready"
423
+ };
424
+ if (!user) return {
425
+ ok: false,
426
+ reason: "missing-user"
427
+ };
428
+ const tour = resolvedSpec.tours[activeTourState.key];
429
+ if (!tour) return {
430
+ ok: false,
431
+ reason: "missing-tour"
432
+ };
433
+ setTourError(null);
434
+ lastNavigatedTourStep.current = null;
435
+ if (activeTourState.stepIndex >= tour.steps.length - 1) {
436
+ const completedKey = activeTourState.key;
437
+ setActiveTourState(null);
438
+ await finishActiveTour(completedKey);
439
+ return { ok: true };
440
+ }
441
+ setActiveTourState({
442
+ key: activeTourState.key,
443
+ stepIndex: activeTourState.stepIndex + 1
444
+ });
445
+ return { ok: true };
446
+ }, [
447
+ activeTourState,
448
+ finishActiveTour,
449
+ resolvedLoading,
450
+ resolvedSpec,
451
+ user
452
+ ]);
453
+ const startChecklistStep = useCallback(async (step) => {
454
+ if (step.action?.type === "startTour") return startTour(step.action.tour);
455
+ await navigateToStep(step);
456
+ }, [navigateToStep, startTour]);
457
+ const visibleHotspots = useMemo(() => {
458
+ if (!resolvedSpec || activeTour) return [];
459
+ return Object.entries(resolvedSpec.hotspots).filter(([, hotspot]) => routeMatches(hotspot.route, currentRoute)).filter(([, hotspot]) => hotspot.repeatable || !effectiveCompletedTourKeys.has(hotspot.action.tour)).map(([key, hotspot]) => ({
460
+ key,
461
+ hotspot
462
+ }));
463
+ }, [
464
+ activeTour,
465
+ effectiveCompletedTourKeys,
466
+ currentRoute,
467
+ resolvedSpec
468
+ ]);
469
+ const reportTourError = useCallback((error) => {
470
+ setTourError(error);
471
+ }, []);
472
+ const value = useMemo(() => ({
473
+ spec: resolvedSpec,
474
+ user,
475
+ loading: resolvedLoading,
476
+ mode: resolvedMode,
477
+ route: currentRoute,
478
+ completedStepKeys: effectiveCompletedStepKeys,
479
+ completedTourKeys: effectiveCompletedTourKeys,
480
+ completedCount,
481
+ totalSteps,
482
+ activeTour,
483
+ tourError,
484
+ track,
485
+ startTour,
486
+ dismissTour,
487
+ advanceTour,
488
+ navigateToStep,
489
+ startChecklistStep,
490
+ reportTourError,
491
+ visibleHotspots
492
+ }), [
493
+ resolvedSpec,
494
+ user,
495
+ resolvedLoading,
496
+ resolvedMode,
497
+ currentRoute,
498
+ effectiveCompletedStepKeys,
499
+ effectiveCompletedTourKeys,
500
+ completedCount,
501
+ totalSteps,
502
+ activeTour,
503
+ tourError,
504
+ track,
505
+ startTour,
506
+ dismissTour,
507
+ advanceTour,
508
+ navigateToStep,
509
+ startChecklistStep,
510
+ reportTourError,
511
+ visibleHotspots
512
+ ]);
513
+ return /* @__PURE__ */ jsx(SunboardContext.Provider, {
514
+ value,
515
+ children
516
+ });
517
+ }
518
+ function useSunboardBootstrap(client, options) {
519
+ const { enabled, environment, origin, route, user } = options;
520
+ const [state, setState] = useState({
521
+ experience: null,
522
+ loading: false
523
+ });
524
+ const warnedSdkAdvisory = useRef(false);
525
+ useEffect(() => {
526
+ if (!client || !enabled || !user?.id) {
527
+ setState({
528
+ experience: null,
529
+ loading: false
530
+ });
531
+ return;
532
+ }
533
+ const controller = new AbortController();
534
+ setState({
535
+ experience: null,
536
+ loading: true
537
+ });
538
+ client.bootstrap({
539
+ environment,
540
+ origin,
541
+ route,
542
+ user: toRuntimeUser(user)
543
+ }).then((response) => {
544
+ if (controller.signal.aborted) return;
545
+ 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.`);
546
+ if (response.sdkAdvisory && isDevEnvironment() && !warnedSdkAdvisory.current) {
547
+ warnedSdkAdvisory.current = true;
548
+ console.warn(`[sunboard] ${response.sdkAdvisory.message}`);
549
+ }
550
+ setState({
551
+ experience: response.experiences[0] ?? null,
552
+ loading: false
553
+ });
554
+ }).catch((error) => {
555
+ if (controller.signal.aborted) return;
556
+ if (isDevEnvironment()) console.warn("[sunboard] bootstrap failed", error);
557
+ setState({
558
+ experience: null,
559
+ loading: false
560
+ });
561
+ });
562
+ return () => {
563
+ controller.abort();
564
+ };
565
+ }, [
566
+ client,
567
+ enabled,
568
+ environment,
569
+ origin,
570
+ user?.id
571
+ ]);
572
+ return state;
573
+ }
574
+ function applyProgress(progress, setSteps, setTours) {
575
+ setSteps((current) => mergeKeys(current, progress.completedStepKeys));
576
+ setTours((current) => mergeKeys(current, progress.completedTourKeys));
577
+ }
578
+ function mergeKeys(current, incoming) {
579
+ if (incoming.length === 0) return current;
580
+ const next = new Set(current);
581
+ for (const key of incoming) next.add(key);
582
+ if (next.size === current.size) return current;
583
+ return next;
584
+ }
585
+ function isDevEnvironment() {
586
+ if (typeof process === "undefined" || !process.env) return false;
587
+ return process.env.NODE_ENV !== "production";
588
+ }
589
+ function toRuntimeUser(user) {
590
+ return {
591
+ email: user.email ?? void 0,
592
+ group: user.group ?? void 0,
593
+ id: user.id,
594
+ properties: user.properties
595
+ };
596
+ }
597
+ function completedToursStorageKey(specKey, userId) {
598
+ return `sunboard:${specKey}:${userId}:completed-tours`;
599
+ }
600
+ function getBrowserRoute() {
601
+ if (typeof window === "undefined") return "/";
602
+ return `${window.location.pathname}${window.location.search}`;
603
+ }
604
+ function getBrowserOrigin() {
605
+ if (typeof window === "undefined") return;
606
+ return window.location.origin;
607
+ }
608
+ function readCompletedTours(storageKey) {
609
+ if (typeof window === "undefined") return /* @__PURE__ */ new Set();
610
+ try {
611
+ const rawValue = window.localStorage.getItem(storageKey);
612
+ const parsed = rawValue ? JSON.parse(rawValue) : [];
613
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
614
+ return new Set(parsed.filter((value) => typeof value === "string"));
615
+ } catch {
616
+ return /* @__PURE__ */ new Set();
617
+ }
618
+ }
619
+ function writeCompletedTours(storageKey, tourKeys) {
620
+ if (typeof window === "undefined") return;
621
+ try {
622
+ window.localStorage.setItem(storageKey, JSON.stringify([...tourKeys].sort()));
623
+ } catch {}
624
+ }
625
+
626
+ //#endregion
627
+ //#region src/lib/interpolate-tokens.ts
628
+ const TOKEN_PATTERN = /\{\{\s*user\.([\w][\w.]*)(?:\s*\|\s*(?:"([^"]*)"|'([^']*)'))?\s*\}\}/g;
629
+ function flattenUser(user) {
630
+ if (!user) return {};
631
+ const flat = { ...user.properties ?? {} };
632
+ flat.id = user.id;
633
+ if (user.email !== void 0) flat.email = user.email;
634
+ if (user.group !== void 0) flat.group = user.group;
635
+ return flat;
636
+ }
637
+ function lookupPath(root, path) {
638
+ const segments = path.split(".");
639
+ let current = root;
640
+ for (const segment of segments) {
641
+ if (current === null || current === void 0) return void 0;
642
+ if (typeof current !== "object") return void 0;
643
+ current = current[segment];
644
+ }
645
+ return current;
646
+ }
647
+ function coerceValue(value) {
648
+ if (value === null || value === void 0) return null;
649
+ if (typeof value === "string") return value.length > 0 ? value : null;
650
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
651
+ return null;
652
+ }
653
+ function interpolateTokens(template, user) {
654
+ if (!template || !template.includes("{{")) return template;
655
+ const flat = flattenUser(user);
656
+ return template.replace(TOKEN_PATTERN, (_match, path, doubleQuoted, singleQuoted) => {
657
+ const resolved = coerceValue(lookupPath(flat, path));
658
+ if (resolved !== null) return resolved;
659
+ return doubleQuoted ?? singleQuoted ?? "";
660
+ });
661
+ }
662
+
663
+ //#endregion
664
+ //#region src/hooks/useSunboard.ts
665
+ function useSunboard() {
666
+ const context = useContext(SunboardContext);
667
+ if (!context) throw new Error("useSunboard must be used within SunboardProvider.");
668
+ return context;
669
+ }
670
+
671
+ //#endregion
672
+ //#region src/hooks/useChecklist.ts
673
+ const SUPPORTED_CHECKLIST_TYPES = new Set(["checklist", "hybrid"]);
674
+ function actionLabelFor(step) {
675
+ if (step.action?.type === "startTour") return "Start";
676
+ if (step.action || step.route) return "Open";
677
+ }
678
+ function interpolateOptional(text, user) {
679
+ if (!text) return void 0;
680
+ return interpolateTokens(text, user) || void 0;
681
+ }
682
+ function useChecklist() {
683
+ const { spec, user, loading, completedStepKeys, completedCount, totalSteps, startChecklistStep } = useSunboard();
684
+ const [dismissedKey, setDismissedKey] = useState(null);
685
+ const hasChecklist = Boolean(spec && SUPPORTED_CHECKLIST_TYPES.has(spec.type));
686
+ const allCompleted = totalSteps > 0 && completedCount >= totalSteps;
687
+ const ready = !loading && !!user && !!spec && hasChecklist;
688
+ const specKey = spec?.key ?? null;
689
+ const userId = user?.id ?? null;
690
+ const observationKey = ready && specKey && userId ? `${specKey}:${userId}` : null;
691
+ const observationRef = useRef(null);
692
+ if (observationKey && observationRef.current?.key !== observationKey) observationRef.current = {
693
+ key: observationKey,
694
+ sawIncomplete: !allCompleted
695
+ };
696
+ else if (observationKey && observationRef.current && !allCompleted) observationRef.current.sawIncomplete = true;
697
+ const sawIncompleteFirst = observationRef.current?.sawIncomplete ?? false;
698
+ const dismissed = dismissedKey !== null && dismissedKey === observationKey;
699
+ const rawSteps = useMemo(() => spec?.checklist?.steps ?? [], [spec]);
700
+ const title = useMemo(() => interpolateOptional(spec?.title, user ?? null), [spec?.title, user]);
701
+ const subtitle = useMemo(() => interpolateOptional(spec?.subtitle, user ?? null), [spec?.subtitle, user]);
702
+ const completion = useMemo(() => {
703
+ const raw = spec?.completion;
704
+ if (!raw) return void 0;
705
+ const interpolatedTitle = interpolateOptional(raw.title, user ?? null);
706
+ if (!interpolatedTitle) return void 0;
707
+ return {
708
+ title: interpolatedTitle,
709
+ subtitle: interpolateOptional(raw.subtitle, user ?? null),
710
+ message: interpolateOptional(raw.message, user ?? null),
711
+ dismissAfterMs: raw.dismissAfterMs
712
+ };
713
+ }, [spec?.completion, user]);
714
+ const showingCompletion = ready && allCompleted && !!completion && sawIncompleteFirst && !dismissed;
715
+ const visible = ready && !allCompleted || showingCompletion;
716
+ const steps = useMemo(() => {
717
+ return rawSteps.map((step) => ({
718
+ key: step.key,
719
+ title: interpolateTokens(step.title, user ?? null),
720
+ description: step.description ? interpolateTokens(step.description, user ?? null) : void 0,
721
+ badge: step.badge ? interpolateTokens(step.badge, user ?? null) : void 0,
722
+ actionLabel: actionLabelFor(step)
723
+ }));
724
+ }, [rawSteps, user]);
725
+ const stepsByKey = useMemo(() => {
726
+ const map = /* @__PURE__ */ new Map();
727
+ for (const step of rawSteps) map.set(step.key, step);
728
+ return map;
729
+ }, [rawSteps]);
730
+ const remainingCount = Math.max(totalSteps - completedCount, 0);
731
+ const progress = totalSteps > 0 ? Math.round(completedCount / totalSteps * 100) : 0;
732
+ const dismissCompletion = useCallback(() => {
733
+ if (observationKey) setDismissedKey(observationKey);
734
+ }, [observationKey]);
735
+ return {
736
+ visible,
737
+ title,
738
+ subtitle,
739
+ steps,
740
+ completedStepKeys,
741
+ completedCount,
742
+ totalSteps,
743
+ remainingCount,
744
+ progress,
745
+ isCompleted: (key) => completedStepKeys.has(key),
746
+ hasAction: (step) => step.actionLabel !== void 0,
747
+ startStep: (step) => {
748
+ const raw = stepsByKey.get(step.key);
749
+ if (raw) startChecklistStep(raw);
750
+ },
751
+ completion: showingCompletion ? completion : void 0,
752
+ dismissCompletion
753
+ };
754
+ }
755
+
756
+ //#endregion
757
+ //#region src/icons/index.tsx
758
+ const CheckIcon = () => {
759
+ return /* @__PURE__ */ jsx("svg", {
760
+ xmlns: "http://www.w3.org/2000/svg",
761
+ width: "24",
762
+ height: "24",
763
+ viewBox: "0 0 24 24",
764
+ fill: "none",
765
+ stroke: "currentColor",
766
+ strokeWidth: "2",
767
+ strokeLinecap: "round",
768
+ strokeLinejoin: "round",
769
+ className: "lucide lucide-check-icon lucide-check",
770
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
771
+ });
772
+ };
773
+
774
+ //#endregion
775
+ //#region src/components/ChecklistStep.tsx
776
+ function ChecklistStep({ completed, onStart, step }) {
777
+ const hasAction = step.actionLabel !== void 0;
778
+ const status = /* @__PURE__ */ jsx("span", {
779
+ className: "sunboard-step-status",
780
+ "aria-label": completed ? "Completed" : "Incomplete",
781
+ children: completed ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
782
+ });
783
+ const content = /* @__PURE__ */ jsxs("div", {
784
+ className: "sunboard-step-content",
785
+ children: [/* @__PURE__ */ jsxs("div", {
786
+ className: "sunboard-step-heading",
787
+ children: [/* @__PURE__ */ jsx("h3", {
788
+ className: "sunboard-step-title",
789
+ children: step.title
790
+ }), step.badge ? /* @__PURE__ */ jsx("span", {
791
+ className: "sunboard-step-badge",
792
+ children: step.badge
793
+ }) : null]
794
+ }), step.description ? /* @__PURE__ */ jsx("p", {
795
+ className: "sunboard-step-description",
796
+ children: step.description
797
+ }) : null]
798
+ });
799
+ return /* @__PURE__ */ jsx("li", {
800
+ className: "sunboard-step",
801
+ "data-completed": completed,
802
+ children: hasAction ? /* @__PURE__ */ jsxs("button", {
803
+ className: "sunboard-step-row",
804
+ type: "button",
805
+ onClick: onStart,
806
+ "aria-label": `${step.actionLabel} ${step.title}`,
807
+ children: [status, content]
808
+ }) : /* @__PURE__ */ jsxs("div", {
809
+ className: "sunboard-step-row",
810
+ children: [status, content]
811
+ })
812
+ });
813
+ }
814
+
815
+ //#endregion
816
+ //#region src/hooks/useInterpolated.ts
817
+ function useInterpolated(text) {
818
+ const { user } = useSunboard();
819
+ return useMemo(() => {
820
+ if (text === void 0) return void 0;
821
+ return interpolateTokens(text, user);
822
+ }, [text, user]);
823
+ }
824
+
825
+ //#endregion
826
+ //#region src/hooks/useTargetElement.ts
827
+ function useTargetElement(selector, enabled, timeoutMs = 5e3) {
828
+ const [state, setState] = useState({
829
+ element: null,
830
+ timedOut: false
831
+ });
832
+ useEffect(() => {
833
+ if (!enabled || !selector) {
834
+ setState({
835
+ element: null,
836
+ timedOut: false
837
+ });
838
+ return;
839
+ }
840
+ let cancelled = false;
841
+ const startedAt = Date.now();
842
+ const findTarget = () => {
843
+ if (cancelled) return;
844
+ const element = document.querySelector(selector);
845
+ if (element instanceof HTMLElement) {
846
+ setState({
847
+ element,
848
+ timedOut: false
849
+ });
850
+ return;
851
+ }
852
+ if (Date.now() - startedAt >= timeoutMs) {
853
+ setState({
854
+ element: null,
855
+ timedOut: true
856
+ });
857
+ return;
858
+ }
859
+ window.setTimeout(findTarget, 100);
860
+ };
861
+ setState({
862
+ element: null,
863
+ timedOut: false
864
+ });
865
+ findTarget();
866
+ return () => {
867
+ cancelled = true;
868
+ };
869
+ }, [
870
+ enabled,
871
+ selector,
872
+ timeoutMs
873
+ ]);
874
+ return state;
875
+ }
876
+
877
+ //#endregion
878
+ //#region src/components/SunboardHotspots.tsx
879
+ const HOTSPOT_RADIUS_PX = 9;
880
+ function SunboardHotspots() {
881
+ const { visibleHotspots } = useSunboard();
882
+ return /* @__PURE__ */ jsx(Fragment, { children: visibleHotspots.map(({ key, hotspot }) => /* @__PURE__ */ jsx(Hotspot, {
883
+ hotspot,
884
+ hotspotKey: key
885
+ }, key)) });
886
+ }
887
+ function Hotspot({ hotspot, hotspotKey }) {
888
+ const { startTour } = useSunboard();
889
+ const label = useInterpolated(hotspot.label);
890
+ const target = useTargetElement(hotspot.target, true);
891
+ const { refs, floatingStyles } = useFloating({
892
+ placement: hotspot.placement,
893
+ strategy: "fixed",
894
+ middleware: [offset(-HOTSPOT_RADIUS_PX)],
895
+ whileElementsMounted: autoUpdate
896
+ });
897
+ useEffect(() => {
898
+ if (!target.element) return;
899
+ refs.setReference(target.element);
900
+ }, [refs, target.element]);
901
+ if (!target.element) return null;
902
+ return /* @__PURE__ */ jsx(FloatingPortal, { children: /* @__PURE__ */ jsx("button", {
903
+ "aria-label": label ?? `Start ${hotspot.action.tour} tour`,
904
+ className: "sunboard-hotspot",
905
+ "data-pulse": hotspot.pulse,
906
+ "data-sunboard-hotspot": hotspotKey,
907
+ onClick: () => {
908
+ startTour(hotspot.action.tour);
909
+ },
910
+ ref: refs.setFloating,
911
+ style: floatingStyles,
912
+ type: "button",
913
+ children: /* @__PURE__ */ jsx("span", { className: "sunboard-hotspot-dot" })
914
+ }) });
915
+ }
916
+
917
+ //#endregion
918
+ //#region src/components/SunboardTour.tsx
919
+ function SunboardTour() {
920
+ const { activeTour, advanceTour, dismissTour, mode, reportTourError, route, tourError } = useSunboard();
921
+ const arrowRef = useRef(null);
922
+ const [spotlightRect, setSpotlightRect] = useState(null);
923
+ const routeReady = routeMatches(activeTour?.step.route, route ?? "/");
924
+ const target = useTargetElement(activeTour?.step.target ?? null, Boolean(activeTour) && routeReady, 1e3);
925
+ const { context, refs, floatingStyles, update } = useFloating({
926
+ placement: activeTour?.step.placement ?? "bottom",
927
+ middleware: [
928
+ offset(16),
929
+ flip(),
930
+ shift({ padding: 12 }),
931
+ arrow({ element: arrowRef })
932
+ ]
933
+ });
934
+ useEffect(() => {
935
+ if (!activeTour) {
936
+ reportTourError(null);
937
+ return;
938
+ }
939
+ if (!routeReady) return;
940
+ if (!target.timedOut) return;
941
+ const error = {
942
+ message: "Sunboard could not find the tour target.",
943
+ route: activeTour.step.route,
944
+ stepKey: activeTour.step.key,
945
+ target: activeTour.step.target,
946
+ tourKey: activeTour.key
947
+ };
948
+ if (mode === "preview") {
949
+ reportTourError(error);
950
+ return;
951
+ }
952
+ reportTourError(null);
953
+ dismissTour();
954
+ }, [
955
+ activeTour,
956
+ dismissTour,
957
+ mode,
958
+ reportTourError,
959
+ routeReady,
960
+ target.timedOut
961
+ ]);
962
+ useEffect(() => {
963
+ if (!activeTour || !target.element) return;
964
+ reportTourError(null);
965
+ refs.setReference(target.element);
966
+ target.element.scrollIntoView({
967
+ block: "center",
968
+ inline: "center"
969
+ });
970
+ update();
971
+ }, [
972
+ activeTour,
973
+ refs,
974
+ reportTourError,
975
+ target.element,
976
+ update
977
+ ]);
978
+ useEffect(() => {
979
+ if (!activeTour || !target.element) {
980
+ setSpotlightRect(null);
981
+ return;
982
+ }
983
+ const updateSpotlight = () => {
984
+ setSpotlightRect(target.element?.getBoundingClientRect() ?? null);
985
+ };
986
+ updateSpotlight();
987
+ window.addEventListener("resize", updateSpotlight);
988
+ window.addEventListener("scroll", updateSpotlight, true);
989
+ target.element.setAttribute("data-sunboard-tour-target", "true");
990
+ return () => {
991
+ window.removeEventListener("resize", updateSpotlight);
992
+ window.removeEventListener("scroll", updateSpotlight, true);
993
+ target.element?.removeAttribute("data-sunboard-tour-target");
994
+ setSpotlightRect(null);
995
+ };
996
+ }, [activeTour, target.element]);
997
+ useEffect(() => {
998
+ if (!activeTour || !target.element) return;
999
+ const advanceOnTargetClick = () => {
1000
+ window.setTimeout(() => {
1001
+ advanceTour();
1002
+ }, 0);
1003
+ };
1004
+ target.element.addEventListener("click", advanceOnTargetClick);
1005
+ return () => {
1006
+ target.element?.removeEventListener("click", advanceOnTargetClick);
1007
+ };
1008
+ }, [
1009
+ activeTour,
1010
+ advanceTour,
1011
+ target.element
1012
+ ]);
1013
+ const tourName = useInterpolated(activeTour?.name);
1014
+ const stepTitle = useInterpolated(activeTour?.step.title);
1015
+ const stepBody = useInterpolated(activeTour?.step.body);
1016
+ if (!activeTour) return null;
1017
+ if (tourError && mode === "preview") return /* @__PURE__ */ jsxs("div", {
1018
+ className: "sunboard-debug",
1019
+ role: "alert",
1020
+ children: [
1021
+ /* @__PURE__ */ jsx("strong", { children: tourError.message }),
1022
+ /* @__PURE__ */ jsxs("span", { children: ["Tour: ", tourError.tourKey] }),
1023
+ /* @__PURE__ */ jsxs("span", { children: ["Step: ", tourError.stepKey] }),
1024
+ tourError.route ? /* @__PURE__ */ jsxs("span", { children: ["Route: ", tourError.route] }) : null,
1025
+ tourError.target ? /* @__PURE__ */ jsxs("span", { children: ["Target: ", tourError.target] }) : null,
1026
+ /* @__PURE__ */ jsx("button", {
1027
+ onClick: () => void dismissTour(),
1028
+ type: "button",
1029
+ children: "Dismiss"
1030
+ })
1031
+ ]
1032
+ });
1033
+ if (!routeReady || !target.element) return null;
1034
+ const isLastStep = activeTour.stepIndex === activeTour.totalSteps - 1;
1035
+ return /* @__PURE__ */ jsxs(FloatingPortal, { children: [/* @__PURE__ */ jsx(SunboardTourScrim, { rect: spotlightRect }), /* @__PURE__ */ jsxs("section", {
1036
+ "aria-label": `${tourName} tour step`,
1037
+ className: "sunboard-tour",
1038
+ ref: refs.setFloating,
1039
+ role: "dialog",
1040
+ style: floatingStyles,
1041
+ children: [
1042
+ /* @__PURE__ */ jsx(FloatingArrow, {
1043
+ className: "sunboard-tour-arrow",
1044
+ context,
1045
+ fill: "var(--sunboard-tour-background)",
1046
+ ref: arrowRef,
1047
+ stroke: "var(--sunboard-tour-border)",
1048
+ strokeWidth: 1,
1049
+ tipRadius: 2
1050
+ }),
1051
+ /* @__PURE__ */ jsxs("div", {
1052
+ className: "sunboard-tour-meta",
1053
+ children: [
1054
+ activeTour.stepIndex + 1,
1055
+ " / ",
1056
+ activeTour.totalSteps
1057
+ ]
1058
+ }),
1059
+ /* @__PURE__ */ jsx("h2", {
1060
+ className: "sunboard-tour-title",
1061
+ children: stepTitle
1062
+ }),
1063
+ stepBody ? /* @__PURE__ */ jsx("p", {
1064
+ className: "sunboard-tour-body",
1065
+ children: stepBody
1066
+ }) : null,
1067
+ /* @__PURE__ */ jsxs("div", {
1068
+ className: "sunboard-tour-actions",
1069
+ children: [/* @__PURE__ */ jsx("button", {
1070
+ className: "sunboard-tour-secondary",
1071
+ onClick: () => void dismissTour(),
1072
+ type: "button",
1073
+ children: "Dismiss"
1074
+ }), /* @__PURE__ */ jsx("button", {
1075
+ className: "sunboard-tour-primary",
1076
+ onClick: () => void advanceTour(),
1077
+ type: "button",
1078
+ children: isLastStep ? "Finish" : "Next"
1079
+ })]
1080
+ })
1081
+ ]
1082
+ })] });
1083
+ }
1084
+ function SunboardTourScrim({ rect }) {
1085
+ if (!rect) return /* @__PURE__ */ jsx("div", { className: "sunboard-tour-scrim" });
1086
+ const padding = 8;
1087
+ const radius = 12;
1088
+ const left = Math.max(rect.left - padding, 0);
1089
+ const top = Math.max(rect.top - padding, 0);
1090
+ const width = rect.width + padding * 2;
1091
+ const height = rect.height + padding * 2;
1092
+ const maskId = "sunboard-tour-spotlight-mask";
1093
+ return /* @__PURE__ */ jsxs("svg", {
1094
+ "aria-hidden": "true",
1095
+ className: "sunboard-tour-scrim sunboard-tour-spotlight",
1096
+ focusable: "false",
1097
+ children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("mask", {
1098
+ id: maskId,
1099
+ children: [/* @__PURE__ */ jsx("rect", {
1100
+ fill: "white",
1101
+ height: "100%",
1102
+ width: "100%",
1103
+ x: "0",
1104
+ y: "0"
1105
+ }), /* @__PURE__ */ jsx("rect", {
1106
+ fill: "black",
1107
+ height,
1108
+ rx: radius,
1109
+ ry: radius,
1110
+ width,
1111
+ x: left,
1112
+ y: top
1113
+ })]
1114
+ }) }), /* @__PURE__ */ jsx("rect", {
1115
+ fill: "var(--sunboard-overlay-color)",
1116
+ height: "100%",
1117
+ mask: `url(#${maskId})`,
1118
+ width: "100%",
1119
+ x: "0",
1120
+ y: "0"
1121
+ })]
1122
+ });
1123
+ }
1124
+
1125
+ //#endregion
1126
+ //#region src/components/Sunboard.tsx
1127
+ function SunboardChecklist() {
1128
+ const [open, setOpen] = useState(true);
1129
+ const { visible, title, subtitle, steps, completedCount, totalSteps, remainingCount, progress, isCompleted, startStep, completion, dismissCompletion } = useChecklist();
1130
+ useEffect(() => {
1131
+ if (!completion?.dismissAfterMs) return;
1132
+ const handle = window.setTimeout(dismissCompletion, completion.dismissAfterMs);
1133
+ return () => window.clearTimeout(handle);
1134
+ }, [completion, dismissCompletion]);
1135
+ if (!visible || !title) return null;
1136
+ if (completion) return /* @__PURE__ */ jsxs("aside", {
1137
+ className: "sunboard-root sunboard-root-completion",
1138
+ "aria-label": `${title} complete`,
1139
+ role: "status",
1140
+ "aria-live": "polite",
1141
+ children: [/* @__PURE__ */ jsx("button", {
1142
+ className: "sunboard-close sunboard-completion-close",
1143
+ type: "button",
1144
+ onClick: dismissCompletion,
1145
+ "aria-label": "Dismiss completion message",
1146
+ children: "x"
1147
+ }), /* @__PURE__ */ jsxs("div", {
1148
+ className: "sunboard-completion",
1149
+ children: [
1150
+ /* @__PURE__ */ jsx("div", {
1151
+ className: "sunboard-completion-check",
1152
+ "aria-hidden": "true",
1153
+ children: /* @__PURE__ */ jsx("svg", {
1154
+ viewBox: "0 0 24 24",
1155
+ fill: "none",
1156
+ stroke: "currentColor",
1157
+ strokeWidth: "3",
1158
+ strokeLinecap: "round",
1159
+ strokeLinejoin: "round",
1160
+ children: /* @__PURE__ */ jsx("polyline", { points: "5 12 10 17 19 8" })
1161
+ })
1162
+ }),
1163
+ completion.subtitle ? /* @__PURE__ */ jsx("p", {
1164
+ className: "sunboard-completion-subtitle",
1165
+ children: completion.subtitle
1166
+ }) : null,
1167
+ /* @__PURE__ */ jsx("h2", {
1168
+ className: "sunboard-completion-title",
1169
+ children: completion.title
1170
+ }),
1171
+ completion.message ? /* @__PURE__ */ jsx("p", {
1172
+ className: "sunboard-completion-message",
1173
+ children: completion.message
1174
+ }) : null
1175
+ ]
1176
+ })]
1177
+ });
1178
+ if (!open) return /* @__PURE__ */ jsx("button", {
1179
+ className: "sunboard-launcher",
1180
+ type: "button",
1181
+ onClick: () => setOpen(true),
1182
+ "aria-label": `Open ${title} checklist, ${remainingCount} tasks remaining`,
1183
+ children: /* @__PURE__ */ jsx("span", {
1184
+ className: "sunboard-launcher-count",
1185
+ children: remainingCount
1186
+ })
1187
+ });
1188
+ return /* @__PURE__ */ jsxs("aside", {
1189
+ className: "sunboard-root",
1190
+ "aria-label": `${title} checklist`,
1191
+ children: [
1192
+ /* @__PURE__ */ jsxs("div", {
1193
+ className: "sunboard-header",
1194
+ children: [/* @__PURE__ */ jsxs("div", { children: [subtitle ? /* @__PURE__ */ jsx("p", {
1195
+ className: "sunboard-eyebrow",
1196
+ children: subtitle
1197
+ }) : null, /* @__PURE__ */ jsx("h2", {
1198
+ className: "sunboard-title",
1199
+ children: title
1200
+ })] }), /* @__PURE__ */ jsxs("div", {
1201
+ className: "sunboard-header-actions",
1202
+ children: [/* @__PURE__ */ jsxs("p", {
1203
+ className: "sunboard-progress-count",
1204
+ "aria-label": "Checklist progress",
1205
+ children: [
1206
+ completedCount,
1207
+ " / ",
1208
+ totalSteps
1209
+ ]
1210
+ }), /* @__PURE__ */ jsx("button", {
1211
+ className: "sunboard-close",
1212
+ type: "button",
1213
+ onClick: () => setOpen(false),
1214
+ "aria-label": `Close ${title} checklist`,
1215
+ children: "x"
1216
+ })]
1217
+ })]
1218
+ }),
1219
+ /* @__PURE__ */ jsx("div", {
1220
+ className: "sunboard-progress",
1221
+ role: "progressbar",
1222
+ "aria-label": `${title} progress`,
1223
+ "aria-valuemin": 0,
1224
+ "aria-valuemax": totalSteps,
1225
+ "aria-valuenow": completedCount,
1226
+ children: /* @__PURE__ */ jsx("div", {
1227
+ className: "sunboard-progress-fill",
1228
+ style: { width: `${progress}%` }
1229
+ })
1230
+ }),
1231
+ /* @__PURE__ */ jsx("ol", {
1232
+ className: "sunboard-step-list",
1233
+ children: steps.map((step) => /* @__PURE__ */ jsx(ChecklistStep, {
1234
+ completed: isCompleted(step.key),
1235
+ onStart: () => startStep(step),
1236
+ step
1237
+ }, step.key))
1238
+ })
1239
+ ]
1240
+ });
1241
+ }
1242
+ const Sunboard = Object.assign(SunboardChecklist, {
1243
+ Checklist: SunboardChecklist,
1244
+ Hotspots: SunboardHotspots,
1245
+ Tour: SunboardTour
1246
+ });
1247
+
1248
+ //#endregion
1249
+ export { Sunboard, SunboardChecklist, SunboardHotspots, SunboardProvider, SunboardRuntimeApiError, SunboardRuntimeClient, SunboardTour, useChecklist, useSunboard, useSunboardPreviewSpec };
1250
+ //# sourceMappingURL=index.mjs.map