@hub-kit/core 0.3.0 → 0.3.2
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/components/tour/index.d.ts +2 -2
- package/dist/components/tour/index.js +1 -1
- package/dist/components/tour/seen-store.d.ts +4 -2
- package/dist/components/tour/seen-store.js +26 -10
- package/dist/components/tour/tour-overlay.js +18 -1
- package/dist/components/tour/tour-provider.d.ts +3 -2
- package/dist/components/tour/tour-provider.js +43 -15
- package/dist/components/tour/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { TourContentBlock, TourDefinition, TourMap, TourPlacement, TourStep, } from "./types";
|
|
1
|
+
export type { TourContentBlock, TourDefinition, TourMap, TourOutcome, TourPlacement, TourSeenStore, TourStep, } from "./types";
|
|
2
2
|
export { TourProvider } from "./tour-provider";
|
|
3
3
|
export type { TourProviderProps } from "./tour-provider";
|
|
4
4
|
export type { TourState } from "./tour-context";
|
|
@@ -11,4 +11,4 @@ export { TourCard } from "./tour-card";
|
|
|
11
11
|
export type { TourCardProps } from "./tour-card";
|
|
12
12
|
export { englishTourLabels } from "./labels";
|
|
13
13
|
export type { TourLabels } from "./labels";
|
|
14
|
-
export { hasSeenTour, markTourSeen, resetSeenTours } from "./seen-store";
|
|
14
|
+
export { hasSeenTour, localTourSeenStore, markTourSeen, resetSeenTours, } from "./seen-store";
|
|
@@ -5,4 +5,4 @@ export { TourButton } from "./tour-button.js";
|
|
|
5
5
|
export { TourOverlay } from "./tour-overlay.js";
|
|
6
6
|
export { TourCard } from "./tour-card.js";
|
|
7
7
|
export { englishTourLabels } from "./labels.js";
|
|
8
|
-
export { hasSeenTour, markTourSeen, resetSeenTours } from "./seen-store.js";
|
|
8
|
+
export { hasSeenTour, localTourSeenStore, markTourSeen, resetSeenTours, } from "./seen-store.js";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
1
|
+
import type { TourSeenStore } from "./types";
|
|
2
|
+
export declare function hasSeenTour(tourId: string, version?: number): boolean;
|
|
3
|
+
export declare function markTourSeen(tourId: string, version?: number): void;
|
|
4
|
+
export declare const localTourSeenStore: TourSeenStore;
|
|
3
5
|
export declare function resetSeenTours(): void;
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
const STORAGE_KEY = "hub-kit.tour.seen";
|
|
2
|
-
function
|
|
2
|
+
function readEntries() {
|
|
3
3
|
try {
|
|
4
4
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
5
5
|
const parsed = raw ? JSON.parse(raw) : null;
|
|
6
|
-
|
|
6
|
+
if (!Array.isArray(parsed)) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
return parsed.flatMap((entry) => {
|
|
10
|
+
if (typeof entry === "string") {
|
|
11
|
+
return [{ id: entry, version: 1 }];
|
|
12
|
+
}
|
|
13
|
+
if (entry && typeof entry === "object" && "id" in entry) {
|
|
14
|
+
const record = entry;
|
|
15
|
+
if (typeof record.id === "string") {
|
|
16
|
+
return [{ id: record.id, version: Number(record.version) || 1 }];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
});
|
|
7
21
|
}
|
|
8
22
|
catch {
|
|
9
23
|
return [];
|
|
10
24
|
}
|
|
11
25
|
}
|
|
12
|
-
export function hasSeenTour(tourId) {
|
|
13
|
-
return
|
|
26
|
+
export function hasSeenTour(tourId, version = 1) {
|
|
27
|
+
return readEntries().some((entry) => entry.id === tourId && entry.version >= version);
|
|
14
28
|
}
|
|
15
|
-
export function markTourSeen(tourId) {
|
|
16
|
-
const
|
|
17
|
-
if (ids.includes(tourId)) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
29
|
+
export function markTourSeen(tourId, version = 1) {
|
|
30
|
+
const entries = readEntries().filter((entry) => entry.id !== tourId);
|
|
20
31
|
try {
|
|
21
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify([...
|
|
32
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([...entries, { id: tourId, version }]));
|
|
22
33
|
}
|
|
23
34
|
catch {
|
|
24
35
|
return;
|
|
25
36
|
}
|
|
26
37
|
}
|
|
38
|
+
export const localTourSeenStore = {
|
|
39
|
+
isReady: true,
|
|
40
|
+
hasSeen: hasSeenTour,
|
|
41
|
+
markSeen: (tourId, version) => markTourSeen(tourId, version),
|
|
42
|
+
};
|
|
27
43
|
export function resetSeenTours() {
|
|
28
44
|
try {
|
|
29
45
|
localStorage.removeItem(STORAGE_KEY);
|
|
@@ -80,6 +80,23 @@ export function TourOverlay() {
|
|
|
80
80
|
wrapper.style.transition = `transform ${MOVE_DURATION} ease`;
|
|
81
81
|
}
|
|
82
82
|
}, [target]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!tour?.isOpen) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const body = document.body;
|
|
88
|
+
const previousOverflow = body.style.overflow;
|
|
89
|
+
const previousPadding = body.style.paddingRight;
|
|
90
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
91
|
+
body.style.overflow = "hidden";
|
|
92
|
+
if (scrollbarWidth > 0) {
|
|
93
|
+
body.style.paddingRight = `${scrollbarWidth}px`;
|
|
94
|
+
}
|
|
95
|
+
return () => {
|
|
96
|
+
body.style.overflow = previousOverflow;
|
|
97
|
+
body.style.paddingRight = previousPadding;
|
|
98
|
+
};
|
|
99
|
+
}, [tour?.isOpen]);
|
|
83
100
|
useEffect(() => {
|
|
84
101
|
if (!tour?.isOpen) {
|
|
85
102
|
return;
|
|
@@ -101,7 +118,7 @@ export function TourOverlay() {
|
|
|
101
118
|
if (!tour || !tour.isOpen || !step || !rect) {
|
|
102
119
|
return null;
|
|
103
120
|
}
|
|
104
|
-
return (_jsxs("div", { className: "fixed inset-0 z-50", role: "presentation", children: [_jsx("div", { className: "absolute inset-0", style: { background: "var(--tour-backdrop, rgb(0 0 0 / 0.5))" } }), _jsx("div", { className: "absolute", style: {
|
|
121
|
+
return (_jsxs("div", { className: "fixed inset-0 z-50", role: "presentation", onClick: tour.skip, children: [_jsx("div", { className: "absolute inset-0", style: { background: "var(--tour-backdrop, rgb(0 0 0 / 0.5))" } }), _jsx("div", { className: "absolute", style: {
|
|
105
122
|
top: rect.top - SPOTLIGHT_PADDING,
|
|
106
123
|
left: rect.left - SPOTLIGHT_PADDING,
|
|
107
124
|
width: rect.width + SPOTLIGHT_PADDING * 2,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type TourLabels } from "./labels";
|
|
3
|
-
import type { TourMap } from "./types";
|
|
3
|
+
import type { TourMap, TourSeenStore } from "./types";
|
|
4
4
|
export interface TourProviderProps {
|
|
5
5
|
tours: TourMap;
|
|
6
6
|
labels?: TourLabels;
|
|
7
|
+
seenStore?: TourSeenStore;
|
|
7
8
|
children: ReactNode;
|
|
8
9
|
}
|
|
9
|
-
export declare function TourProvider({ tours, labels, children }: TourProviderProps): import("react").JSX.Element;
|
|
10
|
+
export declare function TourProvider({ tours, labels, seenStore, children, }: TourProviderProps): import("react").JSX.Element;
|
|
@@ -3,10 +3,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
3
3
|
import { useRouterState } from "@tanstack/react-router";
|
|
4
4
|
import { englishTourLabels } from "./labels.js";
|
|
5
5
|
import { findTourTarget } from "./find-target.js";
|
|
6
|
-
import {
|
|
6
|
+
import { localTourSeenStore } from "./seen-store.js";
|
|
7
7
|
import { TourContext } from "./tour-context.js";
|
|
8
8
|
import { TourOverlay } from "./tour-overlay.js";
|
|
9
|
-
|
|
9
|
+
const STABLE_FRAMES = 10;
|
|
10
|
+
const MAX_WAIT_FRAMES = 600;
|
|
11
|
+
export function TourProvider({ tours, labels = englishTourLabels, seenStore = localTourSeenStore, children, }) {
|
|
10
12
|
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
|
11
13
|
const tour = tours[pathname] ?? null;
|
|
12
14
|
const [openTourId, setOpenTourId] = useState(null);
|
|
@@ -20,20 +22,45 @@ export function TourProvider({ tours, labels = englishTourLabels, children }) {
|
|
|
20
22
|
close();
|
|
21
23
|
}, [pathname, close]);
|
|
22
24
|
const tourId = tour?.id ?? null;
|
|
25
|
+
const tourVersion = tour?.version ?? 1;
|
|
23
26
|
const autoStartAllowed = tour?.autoStart !== false;
|
|
24
27
|
const firstTarget = tour?.steps[0]?.target ?? null;
|
|
28
|
+
const storeIsReady = seenStore.isReady;
|
|
29
|
+
const alreadySeen = tourId !== null && storeIsReady && seenStore.hasSeen(tourId, tourVersion);
|
|
25
30
|
useEffect(() => {
|
|
26
|
-
if (!tourId || !firstTarget || !autoStartAllowed ||
|
|
31
|
+
if (!tourId || !firstTarget || !autoStartAllowed || !storeIsReady || alreadySeen) {
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
let frame = 0;
|
|
35
|
+
let attempts = 0;
|
|
36
|
+
let lastHeight = -1;
|
|
37
|
+
let stableFrames = 0;
|
|
38
|
+
const waitForSettledPage = () => {
|
|
39
|
+
attempts += 1;
|
|
40
|
+
if (attempts > MAX_WAIT_FRAMES) {
|
|
41
|
+
return;
|
|
33
42
|
}
|
|
34
|
-
|
|
43
|
+
const element = findTourTarget(firstTarget);
|
|
44
|
+
if (element) {
|
|
45
|
+
const height = element.getBoundingClientRect().height;
|
|
46
|
+
if (height > 0 && height === lastHeight) {
|
|
47
|
+
stableFrames += 1;
|
|
48
|
+
if (stableFrames >= STABLE_FRAMES) {
|
|
49
|
+
setStepIndex(0);
|
|
50
|
+
setOpenTourId(tourId);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
stableFrames = 0;
|
|
56
|
+
}
|
|
57
|
+
lastHeight = height;
|
|
58
|
+
}
|
|
59
|
+
frame = requestAnimationFrame(waitForSettledPage);
|
|
60
|
+
};
|
|
61
|
+
frame = requestAnimationFrame(waitForSettledPage);
|
|
35
62
|
return () => cancelAnimationFrame(frame);
|
|
36
|
-
}, [tourId, firstTarget, autoStartAllowed]);
|
|
63
|
+
}, [tourId, firstTarget, autoStartAllowed, storeIsReady, alreadySeen]);
|
|
37
64
|
const start = useCallback(() => {
|
|
38
65
|
if (!tour || tour.steps.length === 0) {
|
|
39
66
|
return;
|
|
@@ -41,22 +68,23 @@ export function TourProvider({ tours, labels = englishTourLabels, children }) {
|
|
|
41
68
|
setStepIndex(0);
|
|
42
69
|
setOpenTourId(tour.id);
|
|
43
70
|
}, [tour]);
|
|
44
|
-
const finish = useCallback(() => {
|
|
71
|
+
const finish = useCallback((status) => {
|
|
45
72
|
if (tour) {
|
|
46
|
-
|
|
73
|
+
seenStore.markSeen(tour.id, tour.version ?? 1, { status, lastStep: stepIndex });
|
|
47
74
|
}
|
|
48
75
|
close();
|
|
49
|
-
}, [tour, close]);
|
|
76
|
+
}, [tour, seenStore, stepIndex, close]);
|
|
50
77
|
const next = useCallback(() => {
|
|
51
78
|
if (!tour) {
|
|
52
79
|
return;
|
|
53
80
|
}
|
|
54
81
|
if (stepIndex + 1 >= tour.steps.length) {
|
|
55
|
-
finish();
|
|
82
|
+
finish("completed");
|
|
56
83
|
return;
|
|
57
84
|
}
|
|
58
85
|
setStepIndex(stepIndex + 1);
|
|
59
86
|
}, [tour, stepIndex, finish]);
|
|
87
|
+
const skip = useCallback(() => finish("skipped"), [finish]);
|
|
60
88
|
const back = useCallback(() => {
|
|
61
89
|
setStepIndex((index) => (index > 0 ? index - 1 : index));
|
|
62
90
|
}, []);
|
|
@@ -73,8 +101,8 @@ export function TourProvider({ tours, labels = englishTourLabels, children }) {
|
|
|
73
101
|
start,
|
|
74
102
|
next,
|
|
75
103
|
back,
|
|
76
|
-
skip:
|
|
104
|
+
skip: skip,
|
|
77
105
|
};
|
|
78
|
-
}, [tour, stepIndex, isOpen, labels, start, next, back,
|
|
106
|
+
}, [tour, stepIndex, isOpen, labels, start, next, back, skip]);
|
|
79
107
|
return (_jsxs(TourContext.Provider, { value: value, children: [children, _jsx(TourOverlay, {})] }));
|
|
80
108
|
}
|
|
@@ -40,5 +40,15 @@ export interface TourDefinition {
|
|
|
40
40
|
id: string;
|
|
41
41
|
steps: TourStep[];
|
|
42
42
|
autoStart?: boolean;
|
|
43
|
+
version?: number;
|
|
44
|
+
}
|
|
45
|
+
export type TourOutcome = "skipped" | "completed";
|
|
46
|
+
export interface TourSeenStore {
|
|
47
|
+
isReady: boolean;
|
|
48
|
+
hasSeen: (tourId: string, version: number) => boolean;
|
|
49
|
+
markSeen: (tourId: string, version: number, outcome: {
|
|
50
|
+
status: TourOutcome;
|
|
51
|
+
lastStep: number;
|
|
52
|
+
}) => void;
|
|
43
53
|
}
|
|
44
54
|
export type TourMap = Record<string, TourDefinition>;
|