@flowsterix/react 0.3.1 → 0.4.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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createPathString,
3
3
  notifyRouteChange
4
- } from "./chunk-WTGQUDPT.mjs";
4
+ } from "./chunk-B44EX7YT.mjs";
5
5
 
6
6
  // src/router/tanstackRouterAdapter.tsx
7
7
  import { useRouterState } from "@tanstack/react-router";
@@ -222,6 +222,12 @@ var notifyRouteChange = (path) => {
222
222
  var subscribeToRouteChanges = (listener) => {
223
223
  return routeGatingChannel.subscribe(listener);
224
224
  };
225
+ var matchRoute = (params) => {
226
+ const { pattern, path } = params;
227
+ if (!pattern) return true;
228
+ if (typeof pattern === "string") return path === pattern;
229
+ return pattern.test(path);
230
+ };
225
231
 
226
232
  // src/router/utils.ts
227
233
  var ensurePrefix = (value, prefix) => value.startsWith(prefix) ? value : `${prefix}${value}`;
@@ -278,5 +284,6 @@ export {
278
284
  getCurrentRoutePath,
279
285
  notifyRouteChange,
280
286
  subscribeToRouteChanges,
287
+ matchRoute,
281
288
  createPathString
282
289
  };
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -305,6 +305,127 @@ var supportsMasking = () => {
305
305
  return cachedMaskSupport;
306
306
  };
307
307
 
308
+ // src/router/routeGating.ts
309
+ var DEFAULT_POLL_MS = 150;
310
+ var normalizePathname = (pathname) => {
311
+ if (typeof pathname !== "string" || pathname.length === 0) {
312
+ return "/";
313
+ }
314
+ return pathname.startsWith("/") ? pathname : `/${pathname}`;
315
+ };
316
+ var normalizePrefixedSegment = (value, prefix) => {
317
+ if (typeof value !== "string" || value.length === 0) {
318
+ return "";
319
+ }
320
+ return value.startsWith(prefix) ? value : `${prefix}${value}`;
321
+ };
322
+ var getWindowPath = () => {
323
+ if (!isBrowser) return "/";
324
+ const { pathname, search, hash } = window.location;
325
+ return normalizePathname(pathname) + normalizePrefixedSegment(search, "?") + normalizePrefixedSegment(hash, "#");
326
+ };
327
+ var normalizeExternalPath = (path) => {
328
+ if (path.length === 0) {
329
+ return "/";
330
+ }
331
+ try {
332
+ const parsed = new URL(path, "http://flowsterix.local");
333
+ return normalizePathname(parsed.pathname) + normalizePrefixedSegment(parsed.search, "?") + normalizePrefixedSegment(parsed.hash, "#");
334
+ } catch {
335
+ const [withoutHash, hash = ""] = path.split("#");
336
+ const [base, search = ""] = withoutHash.split("?");
337
+ return normalizePathname(base) + normalizePrefixedSegment(search ? `?${search}` : "", "?") + normalizePrefixedSegment(hash ? `#${hash}` : "", "#");
338
+ }
339
+ };
340
+ var RouteGatingChannel = class {
341
+ #listeners = /* @__PURE__ */ new Set();
342
+ #currentPath = getWindowPath();
343
+ #teardown = null;
344
+ #attachDefaultListeners() {
345
+ if (!isBrowser) return;
346
+ if (this.#teardown) return;
347
+ let lastPath = getWindowPath();
348
+ const emitIfChanged = () => {
349
+ const nextPath = getWindowPath();
350
+ if (nextPath === lastPath) return;
351
+ lastPath = nextPath;
352
+ this.notify(nextPath);
353
+ };
354
+ const handler = () => emitIfChanged();
355
+ window.addEventListener("popstate", handler);
356
+ window.addEventListener("hashchange", handler);
357
+ const pollId = window.setInterval(emitIfChanged, DEFAULT_POLL_MS);
358
+ this.#teardown = () => {
359
+ window.removeEventListener("popstate", handler);
360
+ window.removeEventListener("hashchange", handler);
361
+ window.clearInterval(pollId);
362
+ this.#teardown = null;
363
+ };
364
+ }
365
+ #detachDefaultListeners() {
366
+ if (this.#listeners.size > 0) return;
367
+ this.#teardown?.();
368
+ this.#teardown = null;
369
+ }
370
+ getCurrentPath() {
371
+ if (isBrowser) {
372
+ this.#currentPath = getWindowPath();
373
+ }
374
+ return this.#currentPath;
375
+ }
376
+ notify(path) {
377
+ const resolved = typeof path === "string" && path.length > 0 ? normalizeExternalPath(path) : this.getCurrentPath();
378
+ if (resolved === this.#currentPath) {
379
+ this.#currentPath = resolved;
380
+ return;
381
+ }
382
+ this.#currentPath = resolved;
383
+ for (const listener of Array.from(this.#listeners)) {
384
+ try {
385
+ listener(resolved);
386
+ } catch (error) {
387
+ console.warn("[tour][route-gating] listener error", error);
388
+ }
389
+ }
390
+ }
391
+ subscribe(listener) {
392
+ if (this.#listeners.has(listener)) {
393
+ return () => {
394
+ this.#listeners.delete(listener);
395
+ this.#detachDefaultListeners();
396
+ };
397
+ }
398
+ this.#listeners.add(listener);
399
+ if (this.#listeners.size === 1) {
400
+ this.#attachDefaultListeners();
401
+ }
402
+ const current = this.getCurrentPath();
403
+ try {
404
+ listener(current);
405
+ } catch (error) {
406
+ console.warn("[tour][route-gating] listener error", error);
407
+ }
408
+ return () => {
409
+ this.#listeners.delete(listener);
410
+ this.#detachDefaultListeners();
411
+ };
412
+ }
413
+ };
414
+ var routeGatingChannel = new RouteGatingChannel();
415
+ var getCurrentRoutePath = () => routeGatingChannel.getCurrentPath();
416
+ var notifyRouteChange = (path) => {
417
+ routeGatingChannel.notify(path);
418
+ };
419
+ var subscribeToRouteChanges = (listener) => {
420
+ return routeGatingChannel.subscribe(listener);
421
+ };
422
+ var matchRoute = (params) => {
423
+ const { pattern, path } = params;
424
+ if (!pattern) return true;
425
+ if (typeof pattern === "string") return path === pattern;
426
+ return pattern.test(path);
427
+ };
428
+
308
429
  // src/context.tsx
309
430
  var import_jsx_runtime2 = require("react/jsx-runtime");
310
431
  var TourContext = (0, import_react4.createContext)(void 0);
@@ -352,9 +473,7 @@ var TourProvider = ({
352
473
  );
353
474
  const [debugEnabled, setDebugEnabled] = (0, import_react4.useState)(defaultDebug);
354
475
  const [delayInfo, setDelayInfo] = (0, import_react4.useState)(null);
355
- const autoStartFlow = (0, import_react4.useMemo)(() => {
356
- return flows.find((flow) => flow.autoStart);
357
- }, [flows]);
476
+ const [eligibleAutoStart, setEligibleAutoStart] = (0, import_react4.useState)(null);
358
477
  const teardownStore = (0, import_react4.useCallback)(() => {
359
478
  unsubscribeRef.current?.();
360
479
  unsubscribeRef.current = null;
@@ -558,48 +677,102 @@ var TourProvider = ({
558
677
  [ensureStore, resolveResumeStrategy, runResumeHooks]
559
678
  );
560
679
  (0, import_react4.useEffect)(() => {
561
- if (!autoStartFlow) {
562
- autoStartRequestedRef.current = null;
680
+ const autoStartFlows = flows.filter((f) => f.autoStart);
681
+ if (autoStartFlows.length === 0) {
682
+ setEligibleAutoStart(null);
683
+ return;
684
+ }
685
+ if (!storageAdapter && !fallbackStorageRef.current && isBrowser) {
686
+ fallbackStorageRef.current = (0, import_core.createLocalStorageAdapter)();
687
+ }
688
+ const resolvedStorageAdapter = storageAdapter ?? fallbackStorageRef.current;
689
+ if (!resolvedStorageAdapter) {
690
+ setEligibleAutoStart({
691
+ flow: autoStartFlows[0],
692
+ resolvedState: null,
693
+ stepIndex: 0
694
+ });
563
695
  return;
564
696
  }
565
- if (activeFlowId) return;
566
- if (autoStartRequestedRef.current === autoStartFlow.id) return;
567
- autoStartRequestedRef.current = autoStartFlow.id;
568
697
  let cancelled = false;
569
- const maybeAutoStart = async () => {
570
- if (!storageAdapter && !fallbackStorageRef.current && isBrowser) {
571
- fallbackStorageRef.current = (0, import_core.createLocalStorageAdapter)();
572
- }
573
- const resolvedStorageAdapter = storageAdapter ? storageAdapter : fallbackStorageRef.current;
574
- if (!resolvedStorageAdapter) {
575
- startFlow(autoStartFlow.id, { resume: true });
576
- return;
577
- }
578
- const storageKey = storageNamespace ? `${storageNamespace}:${autoStartFlow.id}` : `${DEFAULT_STORAGE_PREFIX}:${autoStartFlow.id}`;
579
- const snapshot = await (0, import_core.resolveMaybePromise)(
580
- resolvedStorageAdapter.get(storageKey)
698
+ const findEligible = async () => {
699
+ const storageKeys = autoStartFlows.map(
700
+ (f) => storageNamespace ? `${storageNamespace}:${f.id}` : `${DEFAULT_STORAGE_PREFIX}:${f.id}`
701
+ );
702
+ const snapshots = await Promise.all(
703
+ storageKeys.map(
704
+ (key) => (0, import_core.resolveMaybePromise)(resolvedStorageAdapter.get(key))
705
+ )
581
706
  );
582
707
  if (cancelled) return;
583
- const currentVersionStr = (0, import_core.serializeVersion)(autoStartFlow.version);
584
- const storedVersionStr = typeof snapshot?.version === "number" ? (0, import_core.serializeVersion)({ major: snapshot.version, minor: 0 }) : snapshot?.version;
585
- if (snapshot && storedVersionStr === currentVersionStr) {
586
- const storedState = snapshot.value;
587
- const isFinished = storedState.status === "completed";
588
- const isSkipped = storedState.status === "cancelled" && storedState.cancelReason === "skipped";
589
- if (isFinished || isSkipped) {
708
+ for (let i = 0; i < autoStartFlows.length; i++) {
709
+ const flow = autoStartFlows[i];
710
+ const snapshot = snapshots[i];
711
+ if (!snapshot) {
712
+ setEligibleAutoStart({ flow, resolvedState: null, stepIndex: 0 });
590
713
  return;
591
714
  }
715
+ const storedVersionStr = typeof snapshot.version === "number" ? (0, import_core.serializeVersion)({ major: snapshot.version, minor: 0 }) : snapshot.version;
716
+ const storedVersion = (0, import_core.parseVersion)(storedVersionStr);
717
+ const stepIdMap = (0, import_core.buildStepIdMap)(flow);
718
+ const { state: resolvedState } = (0, import_core.handleVersionMismatch)({
719
+ storedState: snapshot.value,
720
+ storedVersion,
721
+ definition: flow,
722
+ currentVersion: flow.version,
723
+ stepIdMap,
724
+ now: () => Date.now()
725
+ });
726
+ if (resolvedState.status === "completed") continue;
727
+ if (resolvedState.status === "cancelled" && resolvedState.cancelReason === "skipped") {
728
+ continue;
729
+ }
730
+ const stepIndex = Math.max(0, resolvedState.stepIndex);
731
+ setEligibleAutoStart({ flow, resolvedState, stepIndex });
732
+ return;
592
733
  }
593
- startFlow(autoStartFlow.id, { resume: true });
734
+ setEligibleAutoStart(null);
594
735
  };
595
- void maybeAutoStart();
736
+ void findEligible();
596
737
  return () => {
597
738
  cancelled = true;
739
+ };
740
+ }, [flows, storageAdapter, storageNamespace]);
741
+ (0, import_react4.useEffect)(() => {
742
+ if (!eligibleAutoStart) {
743
+ autoStartRequestedRef.current = null;
744
+ return;
745
+ }
746
+ if (activeFlowId) return;
747
+ if (autoStartRequestedRef.current === eligibleAutoStart.flow.id) return;
748
+ const { flow, stepIndex } = eligibleAutoStart;
749
+ const step = flow.steps[stepIndex];
750
+ if (!step?.route) {
751
+ autoStartRequestedRef.current = flow.id;
752
+ startFlow(flow.id, { resume: true });
753
+ return;
754
+ }
755
+ const currentPath = getCurrentRoutePath();
756
+ if (matchRoute({ pattern: step.route, path: currentPath })) {
757
+ autoStartRequestedRef.current = flow.id;
758
+ startFlow(flow.id, { resume: true });
759
+ return;
760
+ }
761
+ const unsubscribe = subscribeToRouteChanges((path) => {
762
+ if (activeFlowId) return;
763
+ if (autoStartRequestedRef.current === flow.id) return;
764
+ if (matchRoute({ pattern: step.route, path })) {
765
+ autoStartRequestedRef.current = flow.id;
766
+ startFlow(flow.id, { resume: true });
767
+ }
768
+ });
769
+ return () => {
770
+ unsubscribe();
598
771
  if (!activeFlowId) {
599
772
  autoStartRequestedRef.current = null;
600
773
  }
601
774
  };
602
- }, [activeFlowId, autoStartFlow, startFlow, storageAdapter, storageNamespace]);
775
+ }, [activeFlowId, eligibleAutoStart, startFlow]);
603
776
  const next = (0, import_react4.useCallback)(() => getActiveStore().next(), [getActiveStore]);
604
777
  const back = (0, import_react4.useCallback)(() => getActiveStore().back(), [getActiveStore]);
605
778
  const goToStep = (0, import_react4.useCallback)(
@@ -1493,123 +1666,6 @@ var import_react9 = require("react");
1493
1666
 
1494
1667
  // src/hooks/useAdvanceRules.ts
1495
1668
  var import_react6 = require("react");
1496
-
1497
- // src/router/routeGating.ts
1498
- var DEFAULT_POLL_MS = 150;
1499
- var normalizePathname = (pathname) => {
1500
- if (typeof pathname !== "string" || pathname.length === 0) {
1501
- return "/";
1502
- }
1503
- return pathname.startsWith("/") ? pathname : `/${pathname}`;
1504
- };
1505
- var normalizePrefixedSegment = (value, prefix) => {
1506
- if (typeof value !== "string" || value.length === 0) {
1507
- return "";
1508
- }
1509
- return value.startsWith(prefix) ? value : `${prefix}${value}`;
1510
- };
1511
- var getWindowPath = () => {
1512
- if (!isBrowser) return "/";
1513
- const { pathname, search, hash } = window.location;
1514
- return normalizePathname(pathname) + normalizePrefixedSegment(search, "?") + normalizePrefixedSegment(hash, "#");
1515
- };
1516
- var normalizeExternalPath = (path) => {
1517
- if (path.length === 0) {
1518
- return "/";
1519
- }
1520
- try {
1521
- const parsed = new URL(path, "http://flowsterix.local");
1522
- return normalizePathname(parsed.pathname) + normalizePrefixedSegment(parsed.search, "?") + normalizePrefixedSegment(parsed.hash, "#");
1523
- } catch {
1524
- const [withoutHash, hash = ""] = path.split("#");
1525
- const [base, search = ""] = withoutHash.split("?");
1526
- return normalizePathname(base) + normalizePrefixedSegment(search ? `?${search}` : "", "?") + normalizePrefixedSegment(hash ? `#${hash}` : "", "#");
1527
- }
1528
- };
1529
- var RouteGatingChannel = class {
1530
- #listeners = /* @__PURE__ */ new Set();
1531
- #currentPath = getWindowPath();
1532
- #teardown = null;
1533
- #attachDefaultListeners() {
1534
- if (!isBrowser) return;
1535
- if (this.#teardown) return;
1536
- let lastPath = getWindowPath();
1537
- const emitIfChanged = () => {
1538
- const nextPath = getWindowPath();
1539
- if (nextPath === lastPath) return;
1540
- lastPath = nextPath;
1541
- this.notify(nextPath);
1542
- };
1543
- const handler = () => emitIfChanged();
1544
- window.addEventListener("popstate", handler);
1545
- window.addEventListener("hashchange", handler);
1546
- const pollId = window.setInterval(emitIfChanged, DEFAULT_POLL_MS);
1547
- this.#teardown = () => {
1548
- window.removeEventListener("popstate", handler);
1549
- window.removeEventListener("hashchange", handler);
1550
- window.clearInterval(pollId);
1551
- this.#teardown = null;
1552
- };
1553
- }
1554
- #detachDefaultListeners() {
1555
- if (this.#listeners.size > 0) return;
1556
- this.#teardown?.();
1557
- this.#teardown = null;
1558
- }
1559
- getCurrentPath() {
1560
- if (isBrowser) {
1561
- this.#currentPath = getWindowPath();
1562
- }
1563
- return this.#currentPath;
1564
- }
1565
- notify(path) {
1566
- const resolved = typeof path === "string" && path.length > 0 ? normalizeExternalPath(path) : this.getCurrentPath();
1567
- if (resolved === this.#currentPath) {
1568
- this.#currentPath = resolved;
1569
- return;
1570
- }
1571
- this.#currentPath = resolved;
1572
- for (const listener of Array.from(this.#listeners)) {
1573
- try {
1574
- listener(resolved);
1575
- } catch (error) {
1576
- console.warn("[tour][route-gating] listener error", error);
1577
- }
1578
- }
1579
- }
1580
- subscribe(listener) {
1581
- if (this.#listeners.has(listener)) {
1582
- return () => {
1583
- this.#listeners.delete(listener);
1584
- this.#detachDefaultListeners();
1585
- };
1586
- }
1587
- this.#listeners.add(listener);
1588
- if (this.#listeners.size === 1) {
1589
- this.#attachDefaultListeners();
1590
- }
1591
- const current = this.getCurrentPath();
1592
- try {
1593
- listener(current);
1594
- } catch (error) {
1595
- console.warn("[tour][route-gating] listener error", error);
1596
- }
1597
- return () => {
1598
- this.#listeners.delete(listener);
1599
- this.#detachDefaultListeners();
1600
- };
1601
- }
1602
- };
1603
- var routeGatingChannel = new RouteGatingChannel();
1604
- var getCurrentRoutePath = () => routeGatingChannel.getCurrentPath();
1605
- var notifyRouteChange = (path) => {
1606
- routeGatingChannel.notify(path);
1607
- };
1608
- var subscribeToRouteChanges = (listener) => {
1609
- return routeGatingChannel.subscribe(listener);
1610
- };
1611
-
1612
- // src/hooks/useAdvanceRules.ts
1613
1669
  var DEFAULT_POLL_MS2 = 250;
1614
1670
  var isListenerTarget = (value) => {
1615
1671
  return !!value && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
@@ -3390,7 +3446,7 @@ var TourPopoverPortal = ({
3390
3446
  padding: FLOATING_OFFSET,
3391
3447
  alignment: autoAlignment
3392
3448
  })
3393
- ] : [(0, import_dom13.flip)({ padding: FLOATING_OFFSET })],
3449
+ ] : [(0, import_dom13.flip)({ padding: FLOATING_OFFSET, fallbackStrategy: "bestFit" })],
3394
3450
  (0, import_dom13.shift)({ padding: FLOATING_OFFSET })
3395
3451
  ];
3396
3452
  const updatePosition = async () => {
package/dist/index.mjs CHANGED
@@ -6,16 +6,20 @@ import {
6
6
  getScrollParents,
7
7
  getViewportRect,
8
8
  isBrowser,
9
+ matchRoute,
9
10
  notifyRouteChange,
10
11
  portalHost,
11
12
  subscribeToRouteChanges,
12
13
  supportsMasking
13
- } from "./chunk-WTGQUDPT.mjs";
14
+ } from "./chunk-B44EX7YT.mjs";
14
15
 
15
16
  // src/context.tsx
16
17
  import {
18
+ buildStepIdMap,
17
19
  createFlowStore,
18
20
  createLocalStorageAdapter,
21
+ handleVersionMismatch,
22
+ parseVersion,
19
23
  resolveMaybePromise,
20
24
  serializeVersion
21
25
  } from "@flowsterix/core";
@@ -210,9 +214,7 @@ var TourProvider = ({
210
214
  );
211
215
  const [debugEnabled, setDebugEnabled] = useState2(defaultDebug);
212
216
  const [delayInfo, setDelayInfo] = useState2(null);
213
- const autoStartFlow = useMemo2(() => {
214
- return flows.find((flow) => flow.autoStart);
215
- }, [flows]);
217
+ const [eligibleAutoStart, setEligibleAutoStart] = useState2(null);
216
218
  const teardownStore = useCallback(() => {
217
219
  unsubscribeRef.current?.();
218
220
  unsubscribeRef.current = null;
@@ -416,48 +418,102 @@ var TourProvider = ({
416
418
  [ensureStore, resolveResumeStrategy, runResumeHooks]
417
419
  );
418
420
  useEffect2(() => {
419
- if (!autoStartFlow) {
420
- autoStartRequestedRef.current = null;
421
+ const autoStartFlows = flows.filter((f) => f.autoStart);
422
+ if (autoStartFlows.length === 0) {
423
+ setEligibleAutoStart(null);
424
+ return;
425
+ }
426
+ if (!storageAdapter && !fallbackStorageRef.current && isBrowser) {
427
+ fallbackStorageRef.current = createLocalStorageAdapter();
428
+ }
429
+ const resolvedStorageAdapter = storageAdapter ?? fallbackStorageRef.current;
430
+ if (!resolvedStorageAdapter) {
431
+ setEligibleAutoStart({
432
+ flow: autoStartFlows[0],
433
+ resolvedState: null,
434
+ stepIndex: 0
435
+ });
421
436
  return;
422
437
  }
423
- if (activeFlowId) return;
424
- if (autoStartRequestedRef.current === autoStartFlow.id) return;
425
- autoStartRequestedRef.current = autoStartFlow.id;
426
438
  let cancelled = false;
427
- const maybeAutoStart = async () => {
428
- if (!storageAdapter && !fallbackStorageRef.current && isBrowser) {
429
- fallbackStorageRef.current = createLocalStorageAdapter();
430
- }
431
- const resolvedStorageAdapter = storageAdapter ? storageAdapter : fallbackStorageRef.current;
432
- if (!resolvedStorageAdapter) {
433
- startFlow(autoStartFlow.id, { resume: true });
434
- return;
435
- }
436
- const storageKey = storageNamespace ? `${storageNamespace}:${autoStartFlow.id}` : `${DEFAULT_STORAGE_PREFIX}:${autoStartFlow.id}`;
437
- const snapshot = await resolveMaybePromise(
438
- resolvedStorageAdapter.get(storageKey)
439
+ const findEligible = async () => {
440
+ const storageKeys = autoStartFlows.map(
441
+ (f) => storageNamespace ? `${storageNamespace}:${f.id}` : `${DEFAULT_STORAGE_PREFIX}:${f.id}`
442
+ );
443
+ const snapshots = await Promise.all(
444
+ storageKeys.map(
445
+ (key) => resolveMaybePromise(resolvedStorageAdapter.get(key))
446
+ )
439
447
  );
440
448
  if (cancelled) return;
441
- const currentVersionStr = serializeVersion(autoStartFlow.version);
442
- const storedVersionStr = typeof snapshot?.version === "number" ? serializeVersion({ major: snapshot.version, minor: 0 }) : snapshot?.version;
443
- if (snapshot && storedVersionStr === currentVersionStr) {
444
- const storedState = snapshot.value;
445
- const isFinished = storedState.status === "completed";
446
- const isSkipped = storedState.status === "cancelled" && storedState.cancelReason === "skipped";
447
- if (isFinished || isSkipped) {
449
+ for (let i = 0; i < autoStartFlows.length; i++) {
450
+ const flow = autoStartFlows[i];
451
+ const snapshot = snapshots[i];
452
+ if (!snapshot) {
453
+ setEligibleAutoStart({ flow, resolvedState: null, stepIndex: 0 });
448
454
  return;
449
455
  }
456
+ const storedVersionStr = typeof snapshot.version === "number" ? serializeVersion({ major: snapshot.version, minor: 0 }) : snapshot.version;
457
+ const storedVersion = parseVersion(storedVersionStr);
458
+ const stepIdMap = buildStepIdMap(flow);
459
+ const { state: resolvedState } = handleVersionMismatch({
460
+ storedState: snapshot.value,
461
+ storedVersion,
462
+ definition: flow,
463
+ currentVersion: flow.version,
464
+ stepIdMap,
465
+ now: () => Date.now()
466
+ });
467
+ if (resolvedState.status === "completed") continue;
468
+ if (resolvedState.status === "cancelled" && resolvedState.cancelReason === "skipped") {
469
+ continue;
470
+ }
471
+ const stepIndex = Math.max(0, resolvedState.stepIndex);
472
+ setEligibleAutoStart({ flow, resolvedState, stepIndex });
473
+ return;
450
474
  }
451
- startFlow(autoStartFlow.id, { resume: true });
475
+ setEligibleAutoStart(null);
452
476
  };
453
- void maybeAutoStart();
477
+ void findEligible();
454
478
  return () => {
455
479
  cancelled = true;
480
+ };
481
+ }, [flows, storageAdapter, storageNamespace]);
482
+ useEffect2(() => {
483
+ if (!eligibleAutoStart) {
484
+ autoStartRequestedRef.current = null;
485
+ return;
486
+ }
487
+ if (activeFlowId) return;
488
+ if (autoStartRequestedRef.current === eligibleAutoStart.flow.id) return;
489
+ const { flow, stepIndex } = eligibleAutoStart;
490
+ const step = flow.steps[stepIndex];
491
+ if (!step?.route) {
492
+ autoStartRequestedRef.current = flow.id;
493
+ startFlow(flow.id, { resume: true });
494
+ return;
495
+ }
496
+ const currentPath = getCurrentRoutePath();
497
+ if (matchRoute({ pattern: step.route, path: currentPath })) {
498
+ autoStartRequestedRef.current = flow.id;
499
+ startFlow(flow.id, { resume: true });
500
+ return;
501
+ }
502
+ const unsubscribe = subscribeToRouteChanges((path) => {
503
+ if (activeFlowId) return;
504
+ if (autoStartRequestedRef.current === flow.id) return;
505
+ if (matchRoute({ pattern: step.route, path })) {
506
+ autoStartRequestedRef.current = flow.id;
507
+ startFlow(flow.id, { resume: true });
508
+ }
509
+ });
510
+ return () => {
511
+ unsubscribe();
456
512
  if (!activeFlowId) {
457
513
  autoStartRequestedRef.current = null;
458
514
  }
459
515
  };
460
- }, [activeFlowId, autoStartFlow, startFlow, storageAdapter, storageNamespace]);
516
+ }, [activeFlowId, eligibleAutoStart, startFlow]);
461
517
  const next = useCallback(() => getActiveStore().next(), [getActiveStore]);
462
518
  const back = useCallback(() => getActiveStore().back(), [getActiveStore]);
463
519
  const goToStep = useCallback(
@@ -3137,7 +3193,7 @@ var TourPopoverPortal = ({
3137
3193
  padding: FLOATING_OFFSET,
3138
3194
  alignment: autoAlignment
3139
3195
  })
3140
- ] : [flip({ padding: FLOATING_OFFSET })],
3196
+ ] : [flip({ padding: FLOATING_OFFSET, fallbackStrategy: "bestFit" })],
3141
3197
  shift({ padding: FLOATING_OFFSET })
3142
3198
  ];
3143
3199
  const updatePosition = async () => {
@@ -35,6 +35,7 @@ __export(router_exports, {
35
35
  getCurrentRoutePath: () => getCurrentRoutePath,
36
36
  getTanStackRouter: () => getTanStackRouter,
37
37
  getTourRouter: () => getTourRouter,
38
+ matchRoute: () => matchRoute,
38
39
  notifyRouteChange: () => notifyRouteChange,
39
40
  routeGatingChannel: () => routeGatingChannel,
40
41
  setTanStackRouter: () => setTanStackRouter,
@@ -161,6 +162,12 @@ var notifyRouteChange = (path) => {
161
162
  var subscribeToRouteChanges = (listener) => {
162
163
  return routeGatingChannel.subscribe(listener);
163
164
  };
165
+ var matchRoute = (params) => {
166
+ const { pattern, path } = params;
167
+ if (!pattern) return true;
168
+ if (typeof pattern === "string") return path === pattern;
169
+ return pattern.test(path);
170
+ };
164
171
 
165
172
  // src/router/utils.ts
166
173
  var ensurePrefix = (value, prefix) => value.startsWith(prefix) ? value : `${prefix}${value}`;
@@ -268,6 +275,7 @@ var useTanStackRouterTourAdapter = () => {
268
275
  getCurrentRoutePath,
269
276
  getTanStackRouter,
270
277
  getTourRouter,
278
+ matchRoute,
271
279
  notifyRouteChange,
272
280
  routeGatingChannel,
273
281
  setTanStackRouter,
@@ -1,4 +1,4 @@
1
- export { getCurrentRoutePath, notifyRouteChange, routeGatingChannel, subscribeToRouteChanges, } from './routeGating';
1
+ export { getCurrentRoutePath, matchRoute, notifyRouteChange, routeGatingChannel, subscribeToRouteChanges, } from './routeGating';
2
2
  export { createPathString } from './utils';
3
3
  export { useTanStackRouterTourAdapter } from './tanstackRouterAdapter';
4
4
  export { getTanStackRouter, getTourRouter, setTanStackRouter, setTourRouter, TanStackRouterSync, } from './tanstackRouterSync';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AAEtE,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AAEtE,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,sBAAsB,CAAA"}
@@ -5,20 +5,22 @@ import {
5
5
  setTanStackRouter,
6
6
  setTourRouter,
7
7
  useTanStackRouterTourAdapter
8
- } from "../chunk-L6HQUDEA.mjs";
8
+ } from "../chunk-2ZX2Y3JL.mjs";
9
9
  import {
10
10
  createPathString,
11
11
  getCurrentRoutePath,
12
+ matchRoute,
12
13
  notifyRouteChange,
13
14
  routeGatingChannel,
14
15
  subscribeToRouteChanges
15
- } from "../chunk-WTGQUDPT.mjs";
16
+ } from "../chunk-B44EX7YT.mjs";
16
17
  export {
17
18
  TanStackRouterSync,
18
19
  createPathString,
19
20
  getCurrentRoutePath,
20
21
  getTanStackRouter,
21
22
  getTourRouter,
23
+ matchRoute,
22
24
  notifyRouteChange,
23
25
  routeGatingChannel,
24
26
  setTanStackRouter,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createPathString,
3
3
  notifyRouteChange
4
- } from "../chunk-WTGQUDPT.mjs";
4
+ } from "../chunk-B44EX7YT.mjs";
5
5
 
6
6
  // src/router/nextAppRouterAdapter.tsx
7
7
  import * as NextNavigation from "next/navigation";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createPathString,
3
3
  notifyRouteChange
4
- } from "../chunk-WTGQUDPT.mjs";
4
+ } from "../chunk-B44EX7YT.mjs";
5
5
 
6
6
  // src/router/nextPagesRouterAdapter.tsx
7
7
  import * as NextRouter from "next/router";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createPathString,
3
3
  notifyRouteChange
4
- } from "../chunk-WTGQUDPT.mjs";
4
+ } from "../chunk-B44EX7YT.mjs";
5
5
 
6
6
  // src/router/reactRouterAdapter.tsx
7
7
  import { useEffect } from "react";
@@ -9,5 +9,9 @@ export declare const routeGatingChannel: RouteGatingChannel;
9
9
  export declare const getCurrentRoutePath: () => string;
10
10
  export declare const notifyRouteChange: (path?: string) => void;
11
11
  export declare const subscribeToRouteChanges: (listener: RouteChangeListener) => () => void;
12
+ export declare const matchRoute: (params: {
13
+ pattern: string | RegExp | undefined;
14
+ path: string;
15
+ }) => boolean;
12
16
  export {};
13
17
  //# sourceMappingURL=routeGating.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routeGating.d.ts","sourceRoot":"","sources":["../../src/router/routeGating.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAsDxD,cAAM,kBAAkB;;IAsCtB,cAAc,IAAI,MAAM;IAOxB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM;IAmBpB,SAAS,CAAC,QAAQ,EAAE,mBAAmB;CAyBxC;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAA;AAE1D,eAAO,MAAM,mBAAmB,cAA4C,CAAA;AAE5E,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,SAE9C,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,eAEpE,CAAA"}
1
+ {"version":3,"file":"routeGating.d.ts","sourceRoot":"","sources":["../../src/router/routeGating.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAsDxD,cAAM,kBAAkB;;IAsCtB,cAAc,IAAI,MAAM;IAOxB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM;IAmBpB,SAAS,CAAC,QAAQ,EAAE,mBAAmB;CAyBxC;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAA;AAE1D,eAAO,MAAM,mBAAmB,cAA4C,CAAA;AAE5E,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,SAE9C,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,eAEpE,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,QAAQ;IACjC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACpC,IAAI,EAAE,MAAM,CAAA;CACb,KAAG,OAKH,CAAA"}
@@ -5,8 +5,8 @@ import {
5
5
  setTanStackRouter,
6
6
  setTourRouter,
7
7
  useTanStackRouterTourAdapter
8
- } from "../chunk-L6HQUDEA.mjs";
9
- import "../chunk-WTGQUDPT.mjs";
8
+ } from "../chunk-2ZX2Y3JL.mjs";
9
+ import "../chunk-B44EX7YT.mjs";
10
10
  export {
11
11
  TanStackRouterSync,
12
12
  getTanStackRouter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowsterix/react",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "React bindings for Flowsterix - guided tours and onboarding flows",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  ],
65
65
  "dependencies": {
66
66
  "@floating-ui/dom": "^1.7.4",
67
- "@flowsterix/core": "0.3.0"
67
+ "@flowsterix/core": "0.4.0"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "react": ">=18",