@comitor/ui 1.11.0 → 2.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/LICENSE +35 -0
- package/README.md +23 -7
- package/dist/components/ui/action-bar-constants.d.ts +31 -0
- package/dist/components/ui/action-bar-constants.js +9 -0
- package/dist/components/ui/action-bar-constants.js.map +1 -0
- package/dist/components/ui/action-bar.d.ts +67 -0
- package/dist/components/ui/action-bar.js +522 -0
- package/dist/components/ui/action-bar.js.map +1 -0
- package/dist/components/ui/badge-overflow.d.ts +30 -0
- package/dist/components/ui/badge-overflow.js +179 -0
- package/dist/components/ui/badge-overflow.js.map +1 -0
- package/dist/components/ui/banner.d.ts +79 -0
- package/dist/components/ui/banner.js +518 -0
- package/dist/components/ui/banner.js.map +1 -0
- package/dist/components/ui/circular-progress.d.ts +50 -0
- package/dist/components/ui/circular-progress.js +266 -0
- package/dist/components/ui/circular-progress.js.map +1 -0
- package/dist/components/ui/cropper-constants.d.ts +34 -0
- package/dist/components/ui/cropper-constants.js +10 -0
- package/dist/components/ui/cropper-constants.js.map +1 -0
- package/dist/components/ui/cropper.d.ts +106 -0
- package/dist/components/ui/cropper.js +1269 -0
- package/dist/components/ui/cropper.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +1 -1
- package/dist/components/ui/drawer.js +2 -2
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/kanban-constants.d.ts +61 -0
- package/dist/components/ui/kanban-constants.js +21 -0
- package/dist/components/ui/kanban-constants.js.map +1 -0
- package/dist/components/ui/kanban.d.ts +72 -0
- package/dist/components/ui/kanban.js +787 -0
- package/dist/components/ui/kanban.js.map +1 -0
- package/dist/components/ui/key-value-constants.d.ts +33 -0
- package/dist/components/ui/key-value-constants.js +11 -0
- package/dist/components/ui/key-value-constants.js.map +1 -0
- package/dist/components/ui/key-value.d.ts +96 -0
- package/dist/components/ui/key-value.js +641 -0
- package/dist/components/ui/key-value.js.map +1 -0
- package/dist/components/ui/mask-input-constants.d.ts +99 -0
- package/dist/components/ui/mask-input-constants.js +601 -0
- package/dist/components/ui/mask-input-constants.js.map +1 -0
- package/dist/components/ui/mask-input.d.ts +36 -0
- package/dist/components/ui/mask-input.js +558 -0
- package/dist/components/ui/mask-input.js.map +1 -0
- package/dist/components/ui/mention.d.ts +31 -0
- package/dist/components/ui/mention.js +128 -0
- package/dist/components/ui/mention.js.map +1 -0
- package/dist/components/ui/phone-input-constants.d.ts +54 -0
- package/dist/components/ui/phone-input-constants.js +9 -0
- package/dist/components/ui/phone-input-constants.js.map +1 -0
- package/dist/components/ui/phone-input.d.ts +85 -0
- package/dist/components/ui/phone-input.js +743 -0
- package/dist/components/ui/phone-input.js.map +1 -0
- package/dist/components/ui/rating.d.ts +56 -0
- package/dist/components/ui/rating.js +646 -0
- package/dist/components/ui/rating.js.map +1 -0
- package/dist/components/ui/responsive-dialog.d.ts +35 -0
- package/dist/components/ui/responsive-dialog.js +193 -0
- package/dist/components/ui/responsive-dialog.js.map +1 -0
- package/dist/components/ui/scroll-spy.d.ts +42 -0
- package/dist/components/ui/scroll-spy.js +334 -0
- package/dist/components/ui/scroll-spy.js.map +1 -0
- package/dist/components/ui/stepper-constants.d.ts +29 -0
- package/dist/components/ui/stepper-constants.js +9 -0
- package/dist/components/ui/stepper-constants.js.map +1 -0
- package/dist/components/ui/stepper.d.ts +80 -0
- package/dist/components/ui/stepper.js +902 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/tags-input.d.ts +41 -0
- package/dist/components/ui/tags-input.js +127 -0
- package/dist/components/ui/tags-input.js.map +1 -0
- package/dist/components/ui/time-picker-constants.d.ts +48 -0
- package/dist/components/ui/time-picker-constants.js +14 -0
- package/dist/components/ui/time-picker-constants.js.map +1 -0
- package/dist/components/ui/time-picker.d.ts +115 -0
- package/dist/components/ui/time-picker.js +1620 -0
- package/dist/components/ui/time-picker.js.map +1 -0
- package/dist/components/ui/timeline.d.ts +32 -0
- package/dist/components/ui/timeline.js +559 -0
- package/dist/components/ui/timeline.js.map +1 -0
- package/dist/components/ui/tour-constants.d.ts +39 -0
- package/dist/components/ui/tour-constants.js +12 -0
- package/dist/components/ui/tour-constants.js.map +1 -0
- package/dist/components/ui/tour.d.ts +124 -0
- package/dist/components/ui/tour.js +1207 -0
- package/dist/components/ui/tour.js.map +1 -0
- package/dist/data-grid/data-grid-cell-variants.js +10 -6
- package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
- package/dist/data-grid/data-grid-column-header.js +4 -2
- package/dist/data-grid/data-grid-column-header.js.map +1 -1
- package/dist/data-grid/data-grid-context-menu.js +5 -4
- package/dist/data-grid/data-grid-context-menu.js.map +1 -1
- package/dist/data-grid/data-grid-filter-menu.d.ts +12 -1
- package/dist/data-grid/data-grid-filter-menu.js +28 -17
- package/dist/data-grid/data-grid-filter-menu.js.map +1 -1
- package/dist/data-grid/data-grid-keyboard-shortcuts.d.ts +12 -1
- package/dist/data-grid/data-grid-keyboard-shortcuts.js +7 -5
- package/dist/data-grid/data-grid-keyboard-shortcuts.js.map +1 -1
- package/dist/data-grid/data-grid-locale.d.ts +24 -0
- package/dist/data-grid/data-grid-locale.js +25 -0
- package/dist/data-grid/data-grid-locale.js.map +1 -0
- package/dist/data-grid/data-grid-paste-dialog.js +3 -2
- package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
- package/dist/data-grid/data-grid-row-height-menu.d.ts +11 -1
- package/dist/data-grid/data-grid-row-height-menu.js +25 -28
- package/dist/data-grid/data-grid-row-height-menu.js.map +1 -1
- package/dist/data-grid/data-grid-row.js +1 -1
- package/dist/data-grid/data-grid-row.js.map +1 -1
- package/dist/data-grid/data-grid-search.js +6 -5
- package/dist/data-grid/data-grid-search.js.map +1 -1
- package/dist/data-grid/data-grid-select-column.js +3 -1
- package/dist/data-grid/data-grid-select-column.js.map +1 -1
- package/dist/data-grid/data-grid-sort-menu.d.ts +12 -1
- package/dist/data-grid/data-grid-sort-menu.js +25 -13
- package/dist/data-grid/data-grid-sort-menu.js.map +1 -1
- package/dist/data-grid/data-grid-view-menu.d.ts +12 -1
- package/dist/data-grid/data-grid-view-menu.js +12 -4
- package/dist/data-grid/data-grid-view-menu.js.map +1 -1
- package/dist/data-grid/data-grid.d.ts +14 -2
- package/dist/data-grid/data-grid.js +7 -4
- package/dist/data-grid/data-grid.js.map +1 -1
- package/dist/data-grid/filters.d.ts +17 -21
- package/dist/data-grid/filters.js +73 -52
- package/dist/data-grid/filters.js.map +1 -1
- package/dist/data-grid/index.d.ts +4 -1
- package/dist/data-grid/index.js +20 -2
- package/dist/data-grid/index.js.map +1 -1
- package/dist/data-grid/messages-en.d.ts +294 -0
- package/dist/data-grid/messages-en.js +279 -0
- package/dist/data-grid/messages-en.js.map +1 -0
- package/dist/data-grid/messages-vi.d.ts +12 -0
- package/dist/data-grid/messages-vi.js +279 -0
- package/dist/data-grid/messages-vi.js.map +1 -0
- package/dist/data-grid/messages.d.ts +549 -262
- package/dist/data-grid/messages.js +16 -275
- package/dist/data-grid/messages.js.map +1 -1
- package/dist/data-grid/sortable.js +1 -1
- package/dist/data-grid/sortable.js.map +1 -1
- package/dist/data-grid/use-data-grid-undo-redo.d.ts +13 -1
- package/dist/data-grid/use-data-grid-undo-redo.js +13 -10
- package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
- package/dist/data-grid/use-data-grid.d.ts +281 -1
- package/dist/data-grid/use-data-grid.js +24 -19
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/data-grid/utils.d.ts +9 -1
- package/dist/data-grid/utils.js +3 -2
- package/dist/data-grid/utils.js.map +1 -1
- package/dist/index.d.ts +31 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compose-refs.js.map +1 -0
- package/dist/lib/use-as-ref.js.map +1 -0
- package/dist/lib/use-is-mobile.d.ts +10 -0
- package/dist/lib/use-is-mobile.js +21 -0
- package/dist/lib/use-is-mobile.js.map +1 -0
- package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
- package/dist/lib/use-lazy-ref.js.map +1 -0
- package/dist/lib/visually-hidden-input.d.ts +12 -0
- package/dist/lib/visually-hidden-input.js +105 -0
- package/dist/lib/visually-hidden-input.js.map +1 -0
- package/package.json +5 -1
- package/styles.css +19 -0
- package/dist/data-grid/compose-refs.js.map +0 -1
- package/dist/data-grid/use-as-ref.js.map +0 -1
- package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
- package/dist/data-grid/use-lazy-ref.js.map +0 -1
- /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
- /package/dist/{data-grid → lib}/compose-refs.js +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.js +0 -0
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
autoUpdate,
|
|
5
|
+
flip,
|
|
6
|
+
hide,
|
|
7
|
+
limitShift,
|
|
8
|
+
offset,
|
|
9
|
+
arrow as onArrow,
|
|
10
|
+
shift,
|
|
11
|
+
useFloating
|
|
12
|
+
} from "@floating-ui/react-dom";
|
|
13
|
+
import { useDirection } from "@radix-ui/react-direction";
|
|
14
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
15
|
+
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
|
16
|
+
import * as React from "react";
|
|
17
|
+
import * as ReactDOM from "react-dom";
|
|
18
|
+
import { cn } from "../../lib/cn.js";
|
|
19
|
+
import { useComposedRefs } from "../../lib/compose-refs.js";
|
|
20
|
+
import { useAsRef } from "../../lib/use-as-ref.js";
|
|
21
|
+
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
22
|
+
import { useLazyRef } from "../../lib/use-lazy-ref.js";
|
|
23
|
+
import { Button } from "./button.js";
|
|
24
|
+
import { DEFAULT_TOUR_LABELS } from "./tour-constants.js";
|
|
25
|
+
const ROOT_NAME = "Tour";
|
|
26
|
+
const PORTAL_NAME = "TourPortal";
|
|
27
|
+
const STEP_NAME = "TourStep";
|
|
28
|
+
const ARROW_NAME = "TourArrow";
|
|
29
|
+
const HEADER_NAME = "TourHeader";
|
|
30
|
+
const TITLE_NAME = "TourTitle";
|
|
31
|
+
const DESCRIPTION_NAME = "TourDescription";
|
|
32
|
+
const CLOSE_NAME = "TourClose";
|
|
33
|
+
const PREV_NAME = "TourPrev";
|
|
34
|
+
const NEXT_NAME = "TourNext";
|
|
35
|
+
const SKIP_NAME = "TourSkip";
|
|
36
|
+
const STEP_COUNTER_NAME = "TourStepCounter";
|
|
37
|
+
const FOOTER_NAME = "TourFooter";
|
|
38
|
+
const POINTER_DOWN_OUTSIDE = "tour.pointerDownOutside";
|
|
39
|
+
const INTERACT_OUTSIDE = "tour.interactOutside";
|
|
40
|
+
const OPEN_AUTO_FOCUS = "tour.openAutoFocus";
|
|
41
|
+
const CLOSE_AUTO_FOCUS = "tour.closeAutoFocus";
|
|
42
|
+
const EVENT_OPTIONS = { bubbles: false, cancelable: true };
|
|
43
|
+
const SIDE_OPTIONS = ["top", "right", "bottom", "left"];
|
|
44
|
+
const ALIGN_OPTIONS = ["start", "center", "end"];
|
|
45
|
+
const DEFAULT_ALIGN_OFFSET = 0;
|
|
46
|
+
const DEFAULT_SIDE_OFFSET = 16;
|
|
47
|
+
const DEFAULT_SPOTLIGHT_PADDING = 4;
|
|
48
|
+
const EMPTY_BOUNDARY = [];
|
|
49
|
+
const OPPOSITE_SIDE = {
|
|
50
|
+
top: "bottom",
|
|
51
|
+
right: "left",
|
|
52
|
+
bottom: "top",
|
|
53
|
+
left: "right"
|
|
54
|
+
};
|
|
55
|
+
let focusGuardCount = 0;
|
|
56
|
+
function createFocusGuard() {
|
|
57
|
+
const element = document.createElement("span");
|
|
58
|
+
element.setAttribute("data-tour-focus-guard", "");
|
|
59
|
+
element.tabIndex = 0;
|
|
60
|
+
element.style.outline = "none";
|
|
61
|
+
element.style.opacity = "0";
|
|
62
|
+
element.style.position = "fixed";
|
|
63
|
+
element.style.pointerEvents = "none";
|
|
64
|
+
return element;
|
|
65
|
+
}
|
|
66
|
+
function useFocusGuards(enabled) {
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (!enabled) return;
|
|
69
|
+
const edgeGuards = document.querySelectorAll("[data-tour-focus-guard]");
|
|
70
|
+
document.body.insertAdjacentElement("afterbegin", edgeGuards[0] ?? createFocusGuard());
|
|
71
|
+
document.body.insertAdjacentElement("beforeend", edgeGuards[1] ?? createFocusGuard());
|
|
72
|
+
focusGuardCount++;
|
|
73
|
+
return () => {
|
|
74
|
+
if (focusGuardCount === 1) {
|
|
75
|
+
const guards = document.querySelectorAll("[data-tour-focus-guard]");
|
|
76
|
+
for (const node of guards) {
|
|
77
|
+
node.remove();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
focusGuardCount--;
|
|
81
|
+
};
|
|
82
|
+
}, [enabled]);
|
|
83
|
+
}
|
|
84
|
+
function useFocusTrap(containerRef, enabled, tourOpen, onOpenAutoFocus, onCloseAutoFocus) {
|
|
85
|
+
const lastFocusedElementRef = React.useRef(null);
|
|
86
|
+
const onOpenAutoFocusRef = useAsRef(onOpenAutoFocus);
|
|
87
|
+
const onCloseAutoFocusRef = useAsRef(onCloseAutoFocus);
|
|
88
|
+
const tourOpenRef = useAsRef(tourOpen);
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
if (!enabled) return;
|
|
91
|
+
const container = containerRef.current;
|
|
92
|
+
if (!container) return;
|
|
93
|
+
const previouslyFocusedElement = document.activeElement;
|
|
94
|
+
function getTabbableCandidates() {
|
|
95
|
+
if (!container) return [];
|
|
96
|
+
const nodes = [];
|
|
97
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
|
|
98
|
+
acceptNode: (node) => {
|
|
99
|
+
const element = node;
|
|
100
|
+
const isHiddenInput = element.tagName === "INPUT" && element.type === "hidden";
|
|
101
|
+
const isDisabled = element.disabled === true;
|
|
102
|
+
if (element.hidden || isHiddenInput || isDisabled) return NodeFilter.FILTER_SKIP;
|
|
103
|
+
return element.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
while (walker.nextNode()) {
|
|
107
|
+
nodes.push(walker.currentNode);
|
|
108
|
+
}
|
|
109
|
+
return nodes;
|
|
110
|
+
}
|
|
111
|
+
function getTabbableEdges() {
|
|
112
|
+
const candidates = getTabbableCandidates();
|
|
113
|
+
const first = candidates[0];
|
|
114
|
+
const last = candidates[candidates.length - 1];
|
|
115
|
+
return [first, last];
|
|
116
|
+
}
|
|
117
|
+
function onFocusIn(event) {
|
|
118
|
+
if (!container) return;
|
|
119
|
+
const target = event.target;
|
|
120
|
+
if (container.contains(target)) {
|
|
121
|
+
lastFocusedElementRef.current = target;
|
|
122
|
+
} else {
|
|
123
|
+
const elementToFocus = lastFocusedElementRef.current ?? getTabbableCandidates()[0];
|
|
124
|
+
elementToFocus?.focus({ preventScroll: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function onKeyDown(event) {
|
|
128
|
+
if (event.key !== "Tab" || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
129
|
+
const [first, last] = getTabbableEdges();
|
|
130
|
+
const hasTabbableElements = first && last;
|
|
131
|
+
if (!hasTabbableElements) {
|
|
132
|
+
if (document.activeElement === container) event.preventDefault();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!event.shiftKey && document.activeElement === last) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
first?.focus({ preventScroll: true });
|
|
138
|
+
} else if (event.shiftKey && document.activeElement === first) {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
last?.focus({ preventScroll: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const openAutoFocusEvent = new CustomEvent(OPEN_AUTO_FOCUS, EVENT_OPTIONS);
|
|
144
|
+
if (onOpenAutoFocusRef.current) {
|
|
145
|
+
container.addEventListener(OPEN_AUTO_FOCUS, onOpenAutoFocusRef.current, { once: true });
|
|
146
|
+
}
|
|
147
|
+
container.dispatchEvent(openAutoFocusEvent);
|
|
148
|
+
if (!openAutoFocusEvent.defaultPrevented) {
|
|
149
|
+
const tabbableCandidates = getTabbableCandidates();
|
|
150
|
+
if (tabbableCandidates.length > 0) {
|
|
151
|
+
tabbableCandidates[0]?.focus({ preventScroll: true });
|
|
152
|
+
} else {
|
|
153
|
+
container.focus({ preventScroll: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
document.addEventListener("focusin", onFocusIn);
|
|
157
|
+
container.addEventListener("keydown", onKeyDown);
|
|
158
|
+
return () => {
|
|
159
|
+
document.removeEventListener("focusin", onFocusIn);
|
|
160
|
+
container.removeEventListener("keydown", onKeyDown);
|
|
161
|
+
if (!tourOpenRef.current) {
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
const closeAutoFocusEvent = new CustomEvent(CLOSE_AUTO_FOCUS, EVENT_OPTIONS);
|
|
164
|
+
if (onCloseAutoFocusRef.current) {
|
|
165
|
+
container.addEventListener(CLOSE_AUTO_FOCUS, onCloseAutoFocusRef.current, {
|
|
166
|
+
once: true
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
container.dispatchEvent(closeAutoFocusEvent);
|
|
170
|
+
if (!closeAutoFocusEvent.defaultPrevented) {
|
|
171
|
+
if (previouslyFocusedElement && document.body.contains(previouslyFocusedElement)) {
|
|
172
|
+
previouslyFocusedElement.focus({ preventScroll: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (onCloseAutoFocusRef.current) {
|
|
176
|
+
container.removeEventListener(CLOSE_AUTO_FOCUS, onCloseAutoFocusRef.current);
|
|
177
|
+
}
|
|
178
|
+
}, 0);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}, [containerRef, enabled, onOpenAutoFocusRef, onCloseAutoFocusRef, tourOpenRef]);
|
|
182
|
+
}
|
|
183
|
+
function getDataState(open) {
|
|
184
|
+
return open ? "open" : "closed";
|
|
185
|
+
}
|
|
186
|
+
function useStore(selector, ogStore) {
|
|
187
|
+
const contextStore = React.useContext(StoreContext);
|
|
188
|
+
const store = ogStore ?? contextStore;
|
|
189
|
+
if (!store) {
|
|
190
|
+
throw new Error(`\`useStore\` ph\u1EA3i \u0111\u01B0\u1EE3c g\u1ECDi b\xEAn trong \`${ROOT_NAME}\``);
|
|
191
|
+
}
|
|
192
|
+
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
|
|
193
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
194
|
+
}
|
|
195
|
+
function getTargetElement(target) {
|
|
196
|
+
if (typeof target === "string") {
|
|
197
|
+
return document.querySelector(target);
|
|
198
|
+
}
|
|
199
|
+
if (target && "current" in target) {
|
|
200
|
+
return target.current;
|
|
201
|
+
}
|
|
202
|
+
if (target instanceof HTMLElement) {
|
|
203
|
+
return target;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function getDefaultScrollBehavior() {
|
|
208
|
+
if (typeof window === "undefined") return "smooth";
|
|
209
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth";
|
|
210
|
+
}
|
|
211
|
+
function getScrollParent(element) {
|
|
212
|
+
let parent = element.parentElement;
|
|
213
|
+
while (parent && parent !== document.body && parent !== document.documentElement) {
|
|
214
|
+
const { overflowY } = window.getComputedStyle(parent);
|
|
215
|
+
const isScrollable = overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
|
|
216
|
+
if (isScrollable && parent.scrollHeight > parent.clientHeight) return parent;
|
|
217
|
+
parent = parent.parentElement;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function onScrollToElement(element, scrollBehavior = getDefaultScrollBehavior(), scrollOffset) {
|
|
222
|
+
const offset2 = {
|
|
223
|
+
top: 100,
|
|
224
|
+
bottom: 100,
|
|
225
|
+
left: 0,
|
|
226
|
+
right: 0,
|
|
227
|
+
...scrollOffset
|
|
228
|
+
};
|
|
229
|
+
const rect = element.getBoundingClientRect();
|
|
230
|
+
const viewportHeight = window.innerHeight;
|
|
231
|
+
const viewportWidth = window.innerWidth;
|
|
232
|
+
const isInViewport = rect.top >= offset2.top && rect.bottom <= viewportHeight - offset2.bottom && rect.left >= offset2.left && rect.right <= viewportWidth - offset2.right;
|
|
233
|
+
if (!isInViewport) {
|
|
234
|
+
const scrollParent = getScrollParent(element);
|
|
235
|
+
if (scrollParent) {
|
|
236
|
+
scrollParent.scrollBy({ top: rect.top - offset2.top, behavior: scrollBehavior });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const elementTop = rect.top + window.scrollY;
|
|
240
|
+
const scrollTop = elementTop - offset2.top;
|
|
241
|
+
window.scrollTo({
|
|
242
|
+
top: Math.max(0, scrollTop),
|
|
243
|
+
behavior: scrollBehavior
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function getSideAndAlignFromPlacement(placement) {
|
|
248
|
+
const [side, align = "center"] = placement.split("-");
|
|
249
|
+
return [side, align];
|
|
250
|
+
}
|
|
251
|
+
function getPlacement(side, align) {
|
|
252
|
+
if (align === "center") {
|
|
253
|
+
return side;
|
|
254
|
+
}
|
|
255
|
+
return `${side}-${align}`;
|
|
256
|
+
}
|
|
257
|
+
function updateMask(store, targetElement, padding = DEFAULT_SPOTLIGHT_PADDING) {
|
|
258
|
+
const clientRect = targetElement.getBoundingClientRect();
|
|
259
|
+
const viewportWidth = window.innerWidth;
|
|
260
|
+
const viewportHeight = window.innerHeight;
|
|
261
|
+
const x = Math.max(0, clientRect.left - padding);
|
|
262
|
+
const y = Math.max(0, clientRect.top - padding);
|
|
263
|
+
const width = Math.min(viewportWidth - x, clientRect.width + padding * 2);
|
|
264
|
+
const height = Math.min(viewportHeight - y, clientRect.height + padding * 2);
|
|
265
|
+
const path = `polygon(0% 0%, 0% 100%, ${x}px 100%, ${x}px ${y}px, ${x + width}px ${y}px, ${x + width}px ${y + height}px, ${x}px ${y + height}px, ${x}px 100%, 100% 100%, 100% 0%)`;
|
|
266
|
+
store.setState("maskPath", path);
|
|
267
|
+
store.setState("spotlightRect", { x, y, width, height });
|
|
268
|
+
}
|
|
269
|
+
const StoreContext = React.createContext(null);
|
|
270
|
+
function useStoreContext(consumerName) {
|
|
271
|
+
const context = React.useContext(StoreContext);
|
|
272
|
+
if (!context) {
|
|
273
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
274
|
+
}
|
|
275
|
+
return context;
|
|
276
|
+
}
|
|
277
|
+
const TourContext = React.createContext(null);
|
|
278
|
+
function useTourContext(consumerName) {
|
|
279
|
+
const context = React.useContext(TourContext);
|
|
280
|
+
if (!context) {
|
|
281
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
282
|
+
}
|
|
283
|
+
return context;
|
|
284
|
+
}
|
|
285
|
+
const StepContext = React.createContext(null);
|
|
286
|
+
function useStepContext(consumerName) {
|
|
287
|
+
const context = React.useContext(StepContext);
|
|
288
|
+
if (!context) {
|
|
289
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${STEP_NAME}\``);
|
|
290
|
+
}
|
|
291
|
+
return context;
|
|
292
|
+
}
|
|
293
|
+
const DefaultFooterContext = React.createContext(false);
|
|
294
|
+
const PortalContext = React.createContext(null);
|
|
295
|
+
function usePortalContext(consumerName) {
|
|
296
|
+
const context = React.useContext(PortalContext);
|
|
297
|
+
if (!context) {
|
|
298
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
299
|
+
}
|
|
300
|
+
return context;
|
|
301
|
+
}
|
|
302
|
+
function useScrollLock(enabled) {
|
|
303
|
+
React.useEffect(() => {
|
|
304
|
+
if (!enabled) return;
|
|
305
|
+
const originalStyle = window.getComputedStyle(document.body).overflow;
|
|
306
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
307
|
+
document.body.style.overflow = "hidden";
|
|
308
|
+
if (scrollbarWidth > 0) {
|
|
309
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
310
|
+
}
|
|
311
|
+
return () => {
|
|
312
|
+
document.body.style.overflow = originalStyle;
|
|
313
|
+
document.body.style.paddingRight = "";
|
|
314
|
+
};
|
|
315
|
+
}, [enabled]);
|
|
316
|
+
}
|
|
317
|
+
function Tour(props) {
|
|
318
|
+
const {
|
|
319
|
+
open: openProp,
|
|
320
|
+
defaultOpen = false,
|
|
321
|
+
onOpenChange,
|
|
322
|
+
value: valueProp,
|
|
323
|
+
defaultValue = 0,
|
|
324
|
+
onValueChange,
|
|
325
|
+
onComplete,
|
|
326
|
+
onSkip,
|
|
327
|
+
autoScroll = true,
|
|
328
|
+
/*
|
|
329
|
+
* ⚠ Khác thượng nguồn: KHÔNG có giá trị mặc định ở đây. Bản gốc viết
|
|
330
|
+
* `scrollBehavior = getDefaultScrollBehavior()`, tức đọc `window.matchMedia` NGAY TRONG
|
|
331
|
+
* render. Để `undefined` thì tham số mặc định của `onScrollToElement` tính đúng giá trị đó
|
|
332
|
+
* vào lúc CUỘN — ngoài render, và theo cài đặt reduced-motion ở thời điểm ấy.
|
|
333
|
+
*/
|
|
334
|
+
scrollBehavior,
|
|
335
|
+
scrollOffset,
|
|
336
|
+
onEscapeKeyDown,
|
|
337
|
+
onPointerDownOutside,
|
|
338
|
+
onInteractOutside,
|
|
339
|
+
onOpenAutoFocus,
|
|
340
|
+
onCloseAutoFocus,
|
|
341
|
+
dir: dirProp,
|
|
342
|
+
alignOffset = DEFAULT_ALIGN_OFFSET,
|
|
343
|
+
sideOffset = DEFAULT_SIDE_OFFSET,
|
|
344
|
+
spotlightPadding = DEFAULT_SPOTLIGHT_PADDING,
|
|
345
|
+
dismissible = true,
|
|
346
|
+
modal = true,
|
|
347
|
+
stepFooter,
|
|
348
|
+
labels: labelsProp,
|
|
349
|
+
asChild,
|
|
350
|
+
...rootProps
|
|
351
|
+
} = props;
|
|
352
|
+
const dir = useDirection(dirProp);
|
|
353
|
+
const [portal, setPortal] = React.useState(null);
|
|
354
|
+
const prevOpenRef = React.useRef(void 0);
|
|
355
|
+
const previouslyFocusedElementRef = React.useRef(null);
|
|
356
|
+
const stateRef = useLazyRef(() => ({
|
|
357
|
+
open: openProp ?? defaultOpen,
|
|
358
|
+
value: valueProp ?? defaultValue,
|
|
359
|
+
steps: [],
|
|
360
|
+
maskPath: "",
|
|
361
|
+
spotlightRect: null
|
|
362
|
+
}));
|
|
363
|
+
const listenersRef = useLazyRef(() => /* @__PURE__ */ new Set());
|
|
364
|
+
const stepIdsMapRef = useLazyRef(() => /* @__PURE__ */ new Map());
|
|
365
|
+
const stepIdCounterRef = useLazyRef(() => ({ current: 0 }));
|
|
366
|
+
const propsRef = useAsRef({
|
|
367
|
+
valueProp,
|
|
368
|
+
onOpenChange,
|
|
369
|
+
onValueChange,
|
|
370
|
+
onComplete,
|
|
371
|
+
onSkip,
|
|
372
|
+
onEscapeKeyDown,
|
|
373
|
+
onCloseAutoFocus,
|
|
374
|
+
autoScroll,
|
|
375
|
+
scrollBehavior,
|
|
376
|
+
scrollOffset
|
|
377
|
+
});
|
|
378
|
+
const store = React.useMemo(
|
|
379
|
+
() => ({
|
|
380
|
+
subscribe: (cb) => {
|
|
381
|
+
listenersRef.current.add(cb);
|
|
382
|
+
return () => listenersRef.current.delete(cb);
|
|
383
|
+
},
|
|
384
|
+
getState: () => {
|
|
385
|
+
return stateRef.current;
|
|
386
|
+
},
|
|
387
|
+
setState: (key, value) => {
|
|
388
|
+
if (Object.is(stateRef.current[key], value)) return;
|
|
389
|
+
stateRef.current[key] = value;
|
|
390
|
+
if (key === "open" && typeof value === "boolean") {
|
|
391
|
+
propsRef.current.onOpenChange?.(value);
|
|
392
|
+
if (value) {
|
|
393
|
+
if (stateRef.current.steps.length > 0) {
|
|
394
|
+
if (stateRef.current.value >= stateRef.current.steps.length) {
|
|
395
|
+
store.setState("value", 0);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
if (stateRef.current.value < (stateRef.current.steps.length || 0) - 1) {
|
|
400
|
+
propsRef.current.onSkip?.();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} else if (key === "value" && typeof value === "number") {
|
|
404
|
+
const prevStep = stateRef.current.steps[stateRef.current.value];
|
|
405
|
+
const nextStep = stateRef.current.steps[value];
|
|
406
|
+
prevStep?.onStepLeave?.();
|
|
407
|
+
nextStep?.onStepEnter?.();
|
|
408
|
+
if (value >= stateRef.current.steps.length) {
|
|
409
|
+
propsRef.current.onComplete?.();
|
|
410
|
+
if (propsRef.current.valueProp !== void 0) {
|
|
411
|
+
propsRef.current.onValueChange?.(value);
|
|
412
|
+
}
|
|
413
|
+
store.setState("open", false);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (propsRef.current.valueProp !== void 0) {
|
|
417
|
+
propsRef.current.onValueChange?.(value);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
propsRef.current.onValueChange?.(value);
|
|
421
|
+
if (nextStep && propsRef.current.autoScroll) {
|
|
422
|
+
const targetElement = getTargetElement(nextStep.target);
|
|
423
|
+
if (targetElement) {
|
|
424
|
+
onScrollToElement(targetElement, propsRef.current.scrollBehavior, propsRef.current.scrollOffset);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
store.notify();
|
|
429
|
+
},
|
|
430
|
+
notify: () => {
|
|
431
|
+
for (const listener of listenersRef.current) {
|
|
432
|
+
listener();
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
addStep: (stepData) => {
|
|
436
|
+
const id = `step-${stepIdCounterRef.current.current++}`;
|
|
437
|
+
const index = stateRef.current.steps.length;
|
|
438
|
+
stepIdsMapRef.current.set(id, index);
|
|
439
|
+
stateRef.current.steps = [...stateRef.current.steps, stepData];
|
|
440
|
+
store.notify();
|
|
441
|
+
return { id, index };
|
|
442
|
+
},
|
|
443
|
+
removeStep: (id) => {
|
|
444
|
+
const index = stepIdsMapRef.current.get(id);
|
|
445
|
+
if (index === void 0) return;
|
|
446
|
+
stateRef.current.steps = stateRef.current.steps.filter((_, i) => i !== index);
|
|
447
|
+
stepIdsMapRef.current.delete(id);
|
|
448
|
+
for (const [stepId, stepIndex] of stepIdsMapRef.current.entries()) {
|
|
449
|
+
if (stepIndex > index) {
|
|
450
|
+
stepIdsMapRef.current.set(stepId, stepIndex - 1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
store.notify();
|
|
454
|
+
},
|
|
455
|
+
updateStep: (id, stepData) => {
|
|
456
|
+
const index = stepIdsMapRef.current.get(id);
|
|
457
|
+
if (index === void 0) return;
|
|
458
|
+
if (stateRef.current.steps[index] === stepData) return;
|
|
459
|
+
const steps = [...stateRef.current.steps];
|
|
460
|
+
steps[index] = stepData;
|
|
461
|
+
stateRef.current.steps = steps;
|
|
462
|
+
store.notify();
|
|
463
|
+
},
|
|
464
|
+
getStepIndex: (id) => stepIdsMapRef.current.get(id) ?? -1
|
|
465
|
+
}),
|
|
466
|
+
[stateRef, listenersRef, stepIdsMapRef, stepIdCounterRef, propsRef]
|
|
467
|
+
);
|
|
468
|
+
const open = useStore((state) => state.open, store);
|
|
469
|
+
React.useEffect(() => {
|
|
470
|
+
function onKeyDown(event) {
|
|
471
|
+
if (!open || event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
472
|
+
if (propsRef.current.onEscapeKeyDown) {
|
|
473
|
+
propsRef.current.onEscapeKeyDown(event);
|
|
474
|
+
if (event.defaultPrevented) return;
|
|
475
|
+
}
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
store.setState("open", false);
|
|
478
|
+
}
|
|
479
|
+
document.addEventListener("keydown", onKeyDown, { capture: true });
|
|
480
|
+
return () => document.removeEventListener("keydown", onKeyDown, { capture: true });
|
|
481
|
+
}, [store, open, propsRef]);
|
|
482
|
+
useIsomorphicLayoutEffect(() => {
|
|
483
|
+
const wasOpen = prevOpenRef.current;
|
|
484
|
+
if (open && !wasOpen) {
|
|
485
|
+
previouslyFocusedElementRef.current = document.activeElement;
|
|
486
|
+
} else if (!open && wasOpen) {
|
|
487
|
+
setTimeout(() => {
|
|
488
|
+
const container = portal ?? document.body;
|
|
489
|
+
const closeAutoFocusEvent = new CustomEvent(CLOSE_AUTO_FOCUS, EVENT_OPTIONS);
|
|
490
|
+
if (propsRef.current.onCloseAutoFocus) {
|
|
491
|
+
container.addEventListener(CLOSE_AUTO_FOCUS, propsRef.current.onCloseAutoFocus, {
|
|
492
|
+
once: true
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
container.dispatchEvent(closeAutoFocusEvent);
|
|
496
|
+
if (!closeAutoFocusEvent.defaultPrevented) {
|
|
497
|
+
const elementToFocus = previouslyFocusedElementRef.current;
|
|
498
|
+
if (elementToFocus && document.body.contains(elementToFocus)) {
|
|
499
|
+
elementToFocus.focus({ preventScroll: true });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
previouslyFocusedElementRef.current = null;
|
|
503
|
+
}, 0);
|
|
504
|
+
}
|
|
505
|
+
prevOpenRef.current = open;
|
|
506
|
+
}, [open, portal, propsRef]);
|
|
507
|
+
useIsomorphicLayoutEffect(() => {
|
|
508
|
+
if (openProp !== void 0) {
|
|
509
|
+
store.setState("open", openProp);
|
|
510
|
+
}
|
|
511
|
+
}, [openProp, store]);
|
|
512
|
+
useIsomorphicLayoutEffect(() => {
|
|
513
|
+
if (valueProp !== void 0) {
|
|
514
|
+
store.setState("value", valueProp);
|
|
515
|
+
}
|
|
516
|
+
}, [valueProp, store]);
|
|
517
|
+
const labels = React.useMemo(() => ({ ...DEFAULT_TOUR_LABELS, ...labelsProp }), [labelsProp]);
|
|
518
|
+
const contextValue = React.useMemo(
|
|
519
|
+
() => ({
|
|
520
|
+
dir,
|
|
521
|
+
alignOffset,
|
|
522
|
+
sideOffset,
|
|
523
|
+
spotlightPadding,
|
|
524
|
+
dismissible,
|
|
525
|
+
modal,
|
|
526
|
+
stepFooter,
|
|
527
|
+
labels,
|
|
528
|
+
onPointerDownOutside,
|
|
529
|
+
onInteractOutside,
|
|
530
|
+
onOpenAutoFocus,
|
|
531
|
+
onCloseAutoFocus
|
|
532
|
+
}),
|
|
533
|
+
[
|
|
534
|
+
dir,
|
|
535
|
+
alignOffset,
|
|
536
|
+
sideOffset,
|
|
537
|
+
spotlightPadding,
|
|
538
|
+
dismissible,
|
|
539
|
+
modal,
|
|
540
|
+
stepFooter,
|
|
541
|
+
labels,
|
|
542
|
+
onPointerDownOutside,
|
|
543
|
+
onInteractOutside,
|
|
544
|
+
onOpenAutoFocus,
|
|
545
|
+
onCloseAutoFocus
|
|
546
|
+
]
|
|
547
|
+
);
|
|
548
|
+
const portalContextValue = React.useMemo(
|
|
549
|
+
() => ({
|
|
550
|
+
portal,
|
|
551
|
+
onPortalChange: setPortal
|
|
552
|
+
}),
|
|
553
|
+
[portal]
|
|
554
|
+
);
|
|
555
|
+
useScrollLock(open && modal);
|
|
556
|
+
const RootPrimitive = asChild ? Slot : "div";
|
|
557
|
+
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx(TourContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(PortalContext.Provider, { value: portalContextValue, children: /* @__PURE__ */ jsx(RootPrimitive, { "data-slot": "tour", dir, ...rootProps }) }) }) });
|
|
558
|
+
}
|
|
559
|
+
function TourStep(props) {
|
|
560
|
+
const {
|
|
561
|
+
target,
|
|
562
|
+
side = "bottom",
|
|
563
|
+
sideOffset,
|
|
564
|
+
align = "center",
|
|
565
|
+
alignOffset,
|
|
566
|
+
collisionBoundary = EMPTY_BOUNDARY,
|
|
567
|
+
collisionPadding = 0,
|
|
568
|
+
arrowPadding = 0,
|
|
569
|
+
sticky = "partial",
|
|
570
|
+
hideWhenDetached = false,
|
|
571
|
+
avoidCollisions = true,
|
|
572
|
+
required = false,
|
|
573
|
+
forceMount = false,
|
|
574
|
+
onStepEnter,
|
|
575
|
+
onStepLeave,
|
|
576
|
+
onPointerDownCapture: onPointerDownCaptureProp,
|
|
577
|
+
onFocusCapture: onFocusCaptureProp,
|
|
578
|
+
onBlurCapture: onBlurCaptureProp,
|
|
579
|
+
children,
|
|
580
|
+
className,
|
|
581
|
+
style,
|
|
582
|
+
asChild,
|
|
583
|
+
...stepProps
|
|
584
|
+
} = props;
|
|
585
|
+
const store = useStoreContext(STEP_NAME);
|
|
586
|
+
const [arrow, setArrow] = React.useState(null);
|
|
587
|
+
const [footer, setFooter] = React.useState(null);
|
|
588
|
+
const [targetElement, setTargetElement] = React.useState(null);
|
|
589
|
+
const stepRef = React.useRef(null);
|
|
590
|
+
const stepIdRef = React.useRef("");
|
|
591
|
+
const isPointerInsideReactTreeRef = React.useRef(false);
|
|
592
|
+
const isFocusInsideReactTreeRef = React.useRef(false);
|
|
593
|
+
const titleId = React.useId();
|
|
594
|
+
const descriptionId = React.useId();
|
|
595
|
+
const open = useStore((state) => state.open);
|
|
596
|
+
const value = useStore((state) => state.value);
|
|
597
|
+
const steps = useStore((state) => state.steps);
|
|
598
|
+
const context = useTourContext(STEP_NAME);
|
|
599
|
+
const resolvedSideOffset = sideOffset ?? context.sideOffset;
|
|
600
|
+
const resolvedAlignOffset = alignOffset ?? context.alignOffset;
|
|
601
|
+
const callbacksRef = useAsRef({ onStepEnter, onStepLeave });
|
|
602
|
+
const ownStepData = React.useMemo(
|
|
603
|
+
() => ({
|
|
604
|
+
target,
|
|
605
|
+
align,
|
|
606
|
+
alignOffset: resolvedAlignOffset,
|
|
607
|
+
side,
|
|
608
|
+
sideOffset: resolvedSideOffset,
|
|
609
|
+
collisionBoundary,
|
|
610
|
+
collisionPadding,
|
|
611
|
+
arrowPadding,
|
|
612
|
+
sticky,
|
|
613
|
+
hideWhenDetached,
|
|
614
|
+
avoidCollisions,
|
|
615
|
+
onStepEnter: () => callbacksRef.current.onStepEnter?.(),
|
|
616
|
+
onStepLeave: () => callbacksRef.current.onStepLeave?.(),
|
|
617
|
+
required
|
|
618
|
+
}),
|
|
619
|
+
[
|
|
620
|
+
target,
|
|
621
|
+
align,
|
|
622
|
+
resolvedAlignOffset,
|
|
623
|
+
side,
|
|
624
|
+
resolvedSideOffset,
|
|
625
|
+
collisionBoundary,
|
|
626
|
+
collisionPadding,
|
|
627
|
+
arrowPadding,
|
|
628
|
+
sticky,
|
|
629
|
+
hideWhenDetached,
|
|
630
|
+
avoidCollisions,
|
|
631
|
+
required,
|
|
632
|
+
callbacksRef
|
|
633
|
+
]
|
|
634
|
+
);
|
|
635
|
+
const ownStepDataRef = useAsRef(ownStepData);
|
|
636
|
+
useIsomorphicLayoutEffect(() => {
|
|
637
|
+
const { id } = store.addStep(ownStepDataRef.current);
|
|
638
|
+
stepIdRef.current = id;
|
|
639
|
+
return () => {
|
|
640
|
+
store.removeStep(id);
|
|
641
|
+
};
|
|
642
|
+
}, [store, ownStepDataRef]);
|
|
643
|
+
useIsomorphicLayoutEffect(() => {
|
|
644
|
+
store.updateStep(stepIdRef.current, ownStepData);
|
|
645
|
+
}, [store, ownStepData]);
|
|
646
|
+
const stepData = steps[value];
|
|
647
|
+
const isCurrentStep = store.getStepIndex(stepIdRef.current) === value;
|
|
648
|
+
useIsomorphicLayoutEffect(() => {
|
|
649
|
+
setTargetElement(open && isCurrentStep && stepData ? getTargetElement(stepData.target) : null);
|
|
650
|
+
});
|
|
651
|
+
const isStepRendered = open && isCurrentStep && !!stepData && (!!targetElement || forceMount);
|
|
652
|
+
const middleware = React.useMemo(() => {
|
|
653
|
+
if (!stepData) return [];
|
|
654
|
+
const mainAxisOffset = stepData.sideOffset ?? resolvedSideOffset;
|
|
655
|
+
const crossAxisOffset = stepData.alignOffset ?? resolvedAlignOffset;
|
|
656
|
+
const padding = typeof stepData.collisionPadding === "number" ? stepData.collisionPadding : {
|
|
657
|
+
top: stepData.collisionPadding?.top ?? 0,
|
|
658
|
+
right: stepData.collisionPadding?.right ?? 0,
|
|
659
|
+
bottom: stepData.collisionPadding?.bottom ?? 0,
|
|
660
|
+
left: stepData.collisionPadding?.left ?? 0
|
|
661
|
+
};
|
|
662
|
+
const boundary = Array.isArray(stepData.collisionBoundary) ? stepData.collisionBoundary : stepData.collisionBoundary ? [stepData.collisionBoundary] : [];
|
|
663
|
+
const hasExplicitBoundaries = boundary.length > 0;
|
|
664
|
+
const detectOverflowOptions = {
|
|
665
|
+
padding,
|
|
666
|
+
boundary: boundary.filter((b) => b !== null),
|
|
667
|
+
altBoundary: hasExplicitBoundaries
|
|
668
|
+
};
|
|
669
|
+
return [
|
|
670
|
+
offset({
|
|
671
|
+
mainAxis: mainAxisOffset,
|
|
672
|
+
alignmentAxis: crossAxisOffset
|
|
673
|
+
}),
|
|
674
|
+
stepData.avoidCollisions && shift({
|
|
675
|
+
mainAxis: true,
|
|
676
|
+
crossAxis: false,
|
|
677
|
+
limiter: stepData.sticky === "partial" ? limitShift() : void 0,
|
|
678
|
+
...detectOverflowOptions
|
|
679
|
+
}),
|
|
680
|
+
stepData.avoidCollisions && flip({ ...detectOverflowOptions }),
|
|
681
|
+
arrow && onArrow({ element: arrow, padding: stepData.arrowPadding }),
|
|
682
|
+
stepData.hideWhenDetached && hide({
|
|
683
|
+
strategy: "referenceHidden",
|
|
684
|
+
...detectOverflowOptions
|
|
685
|
+
})
|
|
686
|
+
].filter(Boolean);
|
|
687
|
+
}, [stepData, resolvedSideOffset, resolvedAlignOffset, arrow]);
|
|
688
|
+
const placement = getPlacement(stepData?.side ?? side, stepData?.align ?? align);
|
|
689
|
+
const {
|
|
690
|
+
refs,
|
|
691
|
+
floatingStyles,
|
|
692
|
+
placement: finalPlacement,
|
|
693
|
+
middlewareData
|
|
694
|
+
} = useFloating({
|
|
695
|
+
placement,
|
|
696
|
+
middleware,
|
|
697
|
+
strategy: "fixed",
|
|
698
|
+
whileElementsMounted: autoUpdate,
|
|
699
|
+
elements: {
|
|
700
|
+
reference: targetElement
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
const composedRef = useComposedRefs(refs.setFloating, stepRef);
|
|
704
|
+
const [placedSide, placedAlign] = getSideAndAlignFromPlacement(finalPlacement);
|
|
705
|
+
const arrowX = middlewareData.arrow?.x;
|
|
706
|
+
const arrowY = middlewareData.arrow?.y;
|
|
707
|
+
const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
|
|
708
|
+
const isHidden = hideWhenDetached && middlewareData.hide?.referenceHidden;
|
|
709
|
+
const stepContextValue = React.useMemo(
|
|
710
|
+
() => ({
|
|
711
|
+
arrowX,
|
|
712
|
+
arrowY,
|
|
713
|
+
placedAlign,
|
|
714
|
+
placedSide,
|
|
715
|
+
shouldHideArrow: cannotCenterArrow,
|
|
716
|
+
titleId,
|
|
717
|
+
descriptionId,
|
|
718
|
+
onArrowChange: setArrow,
|
|
719
|
+
onFooterChange: setFooter
|
|
720
|
+
}),
|
|
721
|
+
[arrowX, arrowY, placedSide, placedAlign, cannotCenterArrow, titleId, descriptionId]
|
|
722
|
+
);
|
|
723
|
+
React.useEffect(() => {
|
|
724
|
+
if (open && targetElement && isCurrentStep) {
|
|
725
|
+
let onResize2 = function() {
|
|
726
|
+
if (targetElement) {
|
|
727
|
+
updateMask(store, targetElement, context.spotlightPadding);
|
|
728
|
+
}
|
|
729
|
+
}, onScroll2 = function() {
|
|
730
|
+
if (rafId !== null) return;
|
|
731
|
+
rafId = requestAnimationFrame(() => {
|
|
732
|
+
if (targetElement) {
|
|
733
|
+
updateMask(store, targetElement, context.spotlightPadding);
|
|
734
|
+
}
|
|
735
|
+
rafId = null;
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
var onResize = onResize2, onScroll = onScroll2;
|
|
739
|
+
updateMask(store, targetElement, context.spotlightPadding);
|
|
740
|
+
let rafId = null;
|
|
741
|
+
window.addEventListener("resize", onResize2);
|
|
742
|
+
window.addEventListener("scroll", onScroll2, { capture: true, passive: true });
|
|
743
|
+
return () => {
|
|
744
|
+
window.removeEventListener("resize", onResize2);
|
|
745
|
+
window.removeEventListener("scroll", onScroll2, { capture: true });
|
|
746
|
+
if (rafId !== null) {
|
|
747
|
+
cancelAnimationFrame(rafId);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}, [open, targetElement, isCurrentStep, store, context.spotlightPadding]);
|
|
752
|
+
React.useEffect(() => {
|
|
753
|
+
if (!isStepRendered) return;
|
|
754
|
+
const stepElement = stepRef.current;
|
|
755
|
+
if (!stepElement) return;
|
|
756
|
+
const ownerDocument = stepElement.ownerDocument;
|
|
757
|
+
function onPointerDown(event) {
|
|
758
|
+
if (event.target && !isPointerInsideReactTreeRef.current) {
|
|
759
|
+
const pointerDownOutsideEvent = new CustomEvent(POINTER_DOWN_OUTSIDE, {
|
|
760
|
+
...EVENT_OPTIONS,
|
|
761
|
+
detail: { originalEvent: event }
|
|
762
|
+
});
|
|
763
|
+
context.onPointerDownOutside?.(pointerDownOutsideEvent);
|
|
764
|
+
const interactOutsideEvent = new CustomEvent(INTERACT_OUTSIDE, {
|
|
765
|
+
...EVENT_OPTIONS,
|
|
766
|
+
detail: { originalEvent: event }
|
|
767
|
+
});
|
|
768
|
+
context.onInteractOutside?.(interactOutsideEvent);
|
|
769
|
+
if (!pointerDownOutsideEvent.defaultPrevented && !interactOutsideEvent.defaultPrevented && context.dismissible) {
|
|
770
|
+
store.setState("open", false);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
isPointerInsideReactTreeRef.current = false;
|
|
774
|
+
}
|
|
775
|
+
const timerId = window.setTimeout(() => {
|
|
776
|
+
ownerDocument.addEventListener("pointerdown", onPointerDown);
|
|
777
|
+
}, 0);
|
|
778
|
+
return () => {
|
|
779
|
+
window.clearTimeout(timerId);
|
|
780
|
+
ownerDocument.removeEventListener("pointerdown", onPointerDown);
|
|
781
|
+
};
|
|
782
|
+
}, [isStepRendered, store, context]);
|
|
783
|
+
React.useEffect(() => {
|
|
784
|
+
if (!isStepRendered) return;
|
|
785
|
+
const stepElement = stepRef.current;
|
|
786
|
+
if (!stepElement) return;
|
|
787
|
+
const ownerDocument = stepElement.ownerDocument;
|
|
788
|
+
function onFocusIn(event) {
|
|
789
|
+
const target2 = event.target;
|
|
790
|
+
const isFocusInStep = stepElement?.contains(target2);
|
|
791
|
+
const isFocusInTarget = targetElement?.contains(target2);
|
|
792
|
+
if (event.target && !isFocusInsideReactTreeRef.current && !isFocusInStep && !isFocusInTarget) {
|
|
793
|
+
const interactOutsideEvent = new CustomEvent(INTERACT_OUTSIDE, {
|
|
794
|
+
...EVENT_OPTIONS,
|
|
795
|
+
detail: { originalEvent: event }
|
|
796
|
+
});
|
|
797
|
+
context.onInteractOutside?.(interactOutsideEvent);
|
|
798
|
+
if (!interactOutsideEvent.defaultPrevented && context.dismissible) {
|
|
799
|
+
store.setState("open", false);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
ownerDocument.addEventListener("focusin", onFocusIn);
|
|
804
|
+
return () => {
|
|
805
|
+
ownerDocument.removeEventListener("focusin", onFocusIn);
|
|
806
|
+
};
|
|
807
|
+
}, [isStepRendered, store, context, targetElement]);
|
|
808
|
+
const onPointerDownCapture = React.useCallback(
|
|
809
|
+
(event) => {
|
|
810
|
+
onPointerDownCaptureProp?.(event);
|
|
811
|
+
isPointerInsideReactTreeRef.current = true;
|
|
812
|
+
},
|
|
813
|
+
[onPointerDownCaptureProp]
|
|
814
|
+
);
|
|
815
|
+
const onFocusCapture = React.useCallback(
|
|
816
|
+
(event) => {
|
|
817
|
+
onFocusCaptureProp?.(event);
|
|
818
|
+
isFocusInsideReactTreeRef.current = true;
|
|
819
|
+
},
|
|
820
|
+
[onFocusCaptureProp]
|
|
821
|
+
);
|
|
822
|
+
const onBlurCapture = React.useCallback(
|
|
823
|
+
(event) => {
|
|
824
|
+
onBlurCaptureProp?.(event);
|
|
825
|
+
isFocusInsideReactTreeRef.current = false;
|
|
826
|
+
},
|
|
827
|
+
[onBlurCaptureProp]
|
|
828
|
+
);
|
|
829
|
+
React.useEffect(() => {
|
|
830
|
+
if (!open || !isCurrentStep || !targetElement) return;
|
|
831
|
+
function onTargetPointerDownCapture() {
|
|
832
|
+
isPointerInsideReactTreeRef.current = true;
|
|
833
|
+
}
|
|
834
|
+
function onTargetFocusCapture() {
|
|
835
|
+
isFocusInsideReactTreeRef.current = true;
|
|
836
|
+
}
|
|
837
|
+
function onTargetBlurCapture() {
|
|
838
|
+
isFocusInsideReactTreeRef.current = false;
|
|
839
|
+
}
|
|
840
|
+
targetElement.addEventListener("pointerdown", onTargetPointerDownCapture, true);
|
|
841
|
+
targetElement.addEventListener("focus", onTargetFocusCapture, true);
|
|
842
|
+
targetElement.addEventListener("blur", onTargetBlurCapture, true);
|
|
843
|
+
return () => {
|
|
844
|
+
targetElement.removeEventListener("pointerdown", onTargetPointerDownCapture, true);
|
|
845
|
+
targetElement.removeEventListener("focus", onTargetFocusCapture, true);
|
|
846
|
+
targetElement.removeEventListener("blur", onTargetBlurCapture, true);
|
|
847
|
+
};
|
|
848
|
+
}, [open, isCurrentStep, targetElement]);
|
|
849
|
+
useFocusGuards(isStepRendered);
|
|
850
|
+
useFocusTrap(stepRef, isStepRendered, open, context.onOpenAutoFocus, context.onCloseAutoFocus);
|
|
851
|
+
if (!isStepRendered) {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
const StepPrimitive = asChild ? Slot : "div";
|
|
855
|
+
return /* @__PURE__ */ jsx(StepContext.Provider, { value: stepContextValue, children: /* @__PURE__ */ jsxs(
|
|
856
|
+
StepPrimitive,
|
|
857
|
+
{
|
|
858
|
+
ref: composedRef,
|
|
859
|
+
role: "dialog",
|
|
860
|
+
"aria-labelledby": titleId,
|
|
861
|
+
"aria-describedby": descriptionId,
|
|
862
|
+
"data-slot": "tour-step",
|
|
863
|
+
"data-side": placedSide,
|
|
864
|
+
"data-align": placedAlign,
|
|
865
|
+
dir: context.dir,
|
|
866
|
+
tabIndex: -1,
|
|
867
|
+
...stepProps,
|
|
868
|
+
onPointerDownCapture,
|
|
869
|
+
onFocusCapture,
|
|
870
|
+
onBlurCapture,
|
|
871
|
+
className: cn(
|
|
872
|
+
// Bề mặt cùng khuôn với `PopoverContent`: nền/chữ popover, viền token, bóng tầng nổi.
|
|
873
|
+
"fixed z-50 flex w-80 flex-col gap-4 rounded-lg border bg-popover p-4 text-popover-foreground shadow-overlay outline-none",
|
|
874
|
+
className
|
|
875
|
+
),
|
|
876
|
+
style: {
|
|
877
|
+
...style,
|
|
878
|
+
...floatingStyles,
|
|
879
|
+
visibility: isHidden ? "hidden" : void 0,
|
|
880
|
+
pointerEvents: isHidden ? "none" : void 0
|
|
881
|
+
},
|
|
882
|
+
children: [
|
|
883
|
+
children,
|
|
884
|
+
!footer && /* @__PURE__ */ jsx(DefaultFooterContext.Provider, { value: true, children: context.stepFooter })
|
|
885
|
+
]
|
|
886
|
+
}
|
|
887
|
+
) });
|
|
888
|
+
}
|
|
889
|
+
function TourSpotlight(props) {
|
|
890
|
+
const { asChild, className, style, forceMount = false, ...backdropProps } = props;
|
|
891
|
+
const open = useStore((state) => state.open);
|
|
892
|
+
const maskPath = useStore((state) => state.maskPath);
|
|
893
|
+
if (!open && !forceMount) return null;
|
|
894
|
+
const SpotlightPrimitive = asChild ? Slot : "div";
|
|
895
|
+
return /* @__PURE__ */ jsx(
|
|
896
|
+
SpotlightPrimitive,
|
|
897
|
+
{
|
|
898
|
+
"data-slot": "tour-spotlight",
|
|
899
|
+
"data-state": getDataState(open),
|
|
900
|
+
...backdropProps,
|
|
901
|
+
className: cn(
|
|
902
|
+
/*
|
|
903
|
+
* ⚠ Khác thượng nguồn: `bg-black/80` (bảng màu mặc định Tailwind) → đúng màu lớp phủ của
|
|
904
|
+
* `DialogOverlay`, để lớp tối của tour và của hộp thoại là MỘT thứ trong mắt người dùng.
|
|
905
|
+
* Không lấy `backdrop-blur-sm` của nó: tour cần người dùng còn đọc được ngữ cảnh quanh đích.
|
|
906
|
+
*/
|
|
907
|
+
"fixed inset-0 z-50 bg-ink/60 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 dark:bg-ink/80",
|
|
908
|
+
className
|
|
909
|
+
),
|
|
910
|
+
style: {
|
|
911
|
+
clipPath: maskPath,
|
|
912
|
+
...style
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
function TourSpotlightRing(props) {
|
|
918
|
+
const { asChild, className, style, forceMount = false, ...ringProps } = props;
|
|
919
|
+
const open = useStore((state) => state.open);
|
|
920
|
+
const spotlightRect = useStore((state) => state.spotlightRect);
|
|
921
|
+
if (!open && !forceMount) return null;
|
|
922
|
+
if (!spotlightRect) return null;
|
|
923
|
+
const RingPrimitive = asChild ? Slot : "div";
|
|
924
|
+
return /* @__PURE__ */ jsx(
|
|
925
|
+
RingPrimitive,
|
|
926
|
+
{
|
|
927
|
+
"data-slot": "tour-spotlight-ring",
|
|
928
|
+
"data-state": getDataState(open),
|
|
929
|
+
...ringProps,
|
|
930
|
+
className: cn(
|
|
931
|
+
/*
|
|
932
|
+
* ⚠ Khác thượng nguồn: bỏ `border-ring`. Không có độ dày viền đi kèm nên nó không vẽ gì —
|
|
933
|
+
* chỉ là một màu viền treo sẵn. App muốn viền thì truyền đủ cả độ dày lẫn màu
|
|
934
|
+
* (`border-2 border-primary-ink`), như chính các ví dụ của thượng nguồn vẫn làm.
|
|
935
|
+
*/
|
|
936
|
+
"pointer-events-none fixed z-50 ring-[3px] ring-ring/50",
|
|
937
|
+
className
|
|
938
|
+
),
|
|
939
|
+
style: {
|
|
940
|
+
left: spotlightRect.x,
|
|
941
|
+
top: spotlightRect.y,
|
|
942
|
+
width: spotlightRect.width,
|
|
943
|
+
height: spotlightRect.height,
|
|
944
|
+
...style
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
function TourPortal(props) {
|
|
950
|
+
const { children, container } = props;
|
|
951
|
+
const portalContext = usePortalContext(PORTAL_NAME);
|
|
952
|
+
const [mountNode, setMountNode] = React.useState(null);
|
|
953
|
+
useIsomorphicLayoutEffect(() => {
|
|
954
|
+
const node = container ?? document.body;
|
|
955
|
+
setMountNode(node);
|
|
956
|
+
portalContext?.onPortalChange(node);
|
|
957
|
+
return () => {
|
|
958
|
+
portalContext?.onPortalChange(null);
|
|
959
|
+
};
|
|
960
|
+
}, [container, portalContext]);
|
|
961
|
+
if (!mountNode) return null;
|
|
962
|
+
const portalContainer = container ?? portalContext?.portal ?? mountNode;
|
|
963
|
+
return ReactDOM.createPortal(children, portalContainer);
|
|
964
|
+
}
|
|
965
|
+
function TourArrow(props) {
|
|
966
|
+
const { width = 10, height = 5, className, children, asChild, ...arrowProps } = props;
|
|
967
|
+
const stepContext = useStepContext(ARROW_NAME);
|
|
968
|
+
const baseSide = OPPOSITE_SIDE[stepContext.placedSide];
|
|
969
|
+
return /* @__PURE__ */ jsx(
|
|
970
|
+
"span",
|
|
971
|
+
{
|
|
972
|
+
ref: stepContext.onArrowChange,
|
|
973
|
+
"data-slot": "tour-arrow",
|
|
974
|
+
style: {
|
|
975
|
+
position: "absolute",
|
|
976
|
+
left: stepContext.arrowX != null ? `${stepContext.arrowX}px` : void 0,
|
|
977
|
+
top: stepContext.arrowY != null ? `${stepContext.arrowY}px` : void 0,
|
|
978
|
+
[baseSide]: 0,
|
|
979
|
+
transformOrigin: {
|
|
980
|
+
top: "",
|
|
981
|
+
right: "0 0",
|
|
982
|
+
bottom: "center 0",
|
|
983
|
+
left: "100% 0"
|
|
984
|
+
}[stepContext.placedSide],
|
|
985
|
+
transform: {
|
|
986
|
+
top: "translateY(100%)",
|
|
987
|
+
right: "translateY(50%) rotate(90deg) translateX(-50%)",
|
|
988
|
+
bottom: "rotate(180deg)",
|
|
989
|
+
left: "translateY(50%) rotate(-90deg) translateX(50%)"
|
|
990
|
+
}[stepContext.placedSide],
|
|
991
|
+
visibility: stepContext.shouldHideArrow ? "hidden" : void 0
|
|
992
|
+
},
|
|
993
|
+
children: /* @__PURE__ */ jsx(
|
|
994
|
+
"svg",
|
|
995
|
+
{
|
|
996
|
+
viewBox: "0 0 30 10",
|
|
997
|
+
preserveAspectRatio: "none",
|
|
998
|
+
width,
|
|
999
|
+
height,
|
|
1000
|
+
"aria-hidden": "true",
|
|
1001
|
+
...arrowProps,
|
|
1002
|
+
className: cn("block fill-popover stroke-border", className),
|
|
1003
|
+
children: asChild ? children : /* @__PURE__ */ jsx("polygon", { points: "0,0 30,0 15,10" })
|
|
1004
|
+
}
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
function TourHeader(props) {
|
|
1010
|
+
const { asChild, className, ...headerProps } = props;
|
|
1011
|
+
const context = useTourContext(HEADER_NAME);
|
|
1012
|
+
const HeaderPrimitive = asChild ? Slot : "div";
|
|
1013
|
+
return /* @__PURE__ */ jsx(
|
|
1014
|
+
HeaderPrimitive,
|
|
1015
|
+
{
|
|
1016
|
+
"data-slot": "tour-header",
|
|
1017
|
+
dir: context.dir,
|
|
1018
|
+
...headerProps,
|
|
1019
|
+
className: cn("flex flex-col gap-1.5 text-center sm:text-left", className)
|
|
1020
|
+
}
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
function TourTitle(props) {
|
|
1024
|
+
const { asChild, className, ...titleProps } = props;
|
|
1025
|
+
const context = useTourContext(TITLE_NAME);
|
|
1026
|
+
const stepContext = React.useContext(StepContext);
|
|
1027
|
+
const TitlePrimitive = asChild ? Slot : "div";
|
|
1028
|
+
return /* @__PURE__ */ jsx(
|
|
1029
|
+
TitlePrimitive,
|
|
1030
|
+
{
|
|
1031
|
+
id: stepContext?.titleId,
|
|
1032
|
+
"data-slot": "tour-title",
|
|
1033
|
+
dir: context.dir,
|
|
1034
|
+
...titleProps,
|
|
1035
|
+
className: cn("text-lg leading-none font-semibold tracking-tight", className)
|
|
1036
|
+
}
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
function TourDescription(props) {
|
|
1040
|
+
const { asChild, className, ...descriptionProps } = props;
|
|
1041
|
+
const context = useTourContext(DESCRIPTION_NAME);
|
|
1042
|
+
const stepContext = React.useContext(StepContext);
|
|
1043
|
+
const DescriptionPrimitive = asChild ? Slot : "div";
|
|
1044
|
+
return /* @__PURE__ */ jsx(
|
|
1045
|
+
DescriptionPrimitive,
|
|
1046
|
+
{
|
|
1047
|
+
id: stepContext?.descriptionId,
|
|
1048
|
+
"data-slot": "tour-description",
|
|
1049
|
+
dir: context.dir,
|
|
1050
|
+
...descriptionProps,
|
|
1051
|
+
className: cn("text-sm text-muted-foreground", className)
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
function TourClose(props) {
|
|
1056
|
+
const { asChild, className, children, onClick: onClickProp, ...closeButtonProps } = props;
|
|
1057
|
+
const store = useStoreContext(CLOSE_NAME);
|
|
1058
|
+
const context = useTourContext(CLOSE_NAME);
|
|
1059
|
+
const onClick = React.useCallback(
|
|
1060
|
+
(event) => {
|
|
1061
|
+
onClickProp?.(event);
|
|
1062
|
+
if (event.defaultPrevented) return;
|
|
1063
|
+
store.setState("open", false);
|
|
1064
|
+
},
|
|
1065
|
+
[store, onClickProp]
|
|
1066
|
+
);
|
|
1067
|
+
const ClosePrimitive = asChild ? Slot : "button";
|
|
1068
|
+
return /* @__PURE__ */ jsx(
|
|
1069
|
+
ClosePrimitive,
|
|
1070
|
+
{
|
|
1071
|
+
type: "button",
|
|
1072
|
+
"aria-label": context.labels.close,
|
|
1073
|
+
"data-slot": "tour-close",
|
|
1074
|
+
className: cn(
|
|
1075
|
+
/*
|
|
1076
|
+
* ⚠ Khác thượng nguồn: khuôn nút đóng của `DialogContent` (màu chữ muted, hover nền muted,
|
|
1077
|
+
* vòng focus của gói) thay cho `opacity-70` — `opacity` không miễn ngưỡng tương phản cho
|
|
1078
|
+
* thứ còn bấm được (docs/hop-dong-mau.md). `top-3 right-3 p-1` đặt icon đúng chỗ
|
|
1079
|
+
* `top-4 right-4` cũ nhưng vùng bấm lên ~24px thay vì 16px (WCAG 2.5.8).
|
|
1080
|
+
*/
|
|
1081
|
+
"absolute top-3 right-3 rounded-md p-1 text-muted-foreground transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
1082
|
+
className
|
|
1083
|
+
),
|
|
1084
|
+
onClick,
|
|
1085
|
+
...closeButtonProps,
|
|
1086
|
+
children: children ?? /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
function TourPrev(props) {
|
|
1091
|
+
const { children, onClick: onClickProp, ...prevButtonProps } = props;
|
|
1092
|
+
const store = useStoreContext(PREV_NAME);
|
|
1093
|
+
const context = useTourContext(PREV_NAME);
|
|
1094
|
+
const value = useStore((state) => state.value);
|
|
1095
|
+
const onClick = React.useCallback(
|
|
1096
|
+
(event) => {
|
|
1097
|
+
onClickProp?.(event);
|
|
1098
|
+
if (event.defaultPrevented) return;
|
|
1099
|
+
if (value > 0) {
|
|
1100
|
+
store.setState("value", value - 1);
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
[value, store, onClickProp]
|
|
1104
|
+
);
|
|
1105
|
+
return /* @__PURE__ */ jsx(
|
|
1106
|
+
Button,
|
|
1107
|
+
{
|
|
1108
|
+
type: "button",
|
|
1109
|
+
"data-slot": "tour-prev",
|
|
1110
|
+
variant: "outline",
|
|
1111
|
+
...prevButtonProps,
|
|
1112
|
+
onClick,
|
|
1113
|
+
disabled: value === 0,
|
|
1114
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1115
|
+
/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true", className: "size-4" }),
|
|
1116
|
+
context.labels.prev
|
|
1117
|
+
] })
|
|
1118
|
+
}
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function TourNext(props) {
|
|
1122
|
+
const { children, onClick: onClickProp, ...nextButtonProps } = props;
|
|
1123
|
+
const store = useStoreContext(NEXT_NAME);
|
|
1124
|
+
const context = useTourContext(NEXT_NAME);
|
|
1125
|
+
const value = useStore((state) => state.value);
|
|
1126
|
+
const steps = useStore((state) => state.steps);
|
|
1127
|
+
const isLastStep = value === steps.length - 1;
|
|
1128
|
+
const onClick = React.useCallback(
|
|
1129
|
+
(event) => {
|
|
1130
|
+
onClickProp?.(event);
|
|
1131
|
+
if (event.defaultPrevented) return;
|
|
1132
|
+
store.setState("value", value + 1);
|
|
1133
|
+
},
|
|
1134
|
+
[value, store, onClickProp]
|
|
1135
|
+
);
|
|
1136
|
+
return /* @__PURE__ */ jsx(Button, { type: "button", "data-slot": "tour-next", ...nextButtonProps, onClick, children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1137
|
+
isLastStep ? context.labels.finish : context.labels.next,
|
|
1138
|
+
!isLastStep && /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-4" })
|
|
1139
|
+
] }) });
|
|
1140
|
+
}
|
|
1141
|
+
function TourSkip(props) {
|
|
1142
|
+
const { children, onClick: onClickProp, ...skipButtonProps } = props;
|
|
1143
|
+
const store = useStoreContext(SKIP_NAME);
|
|
1144
|
+
const context = useTourContext(SKIP_NAME);
|
|
1145
|
+
const onClick = React.useCallback(
|
|
1146
|
+
(event) => {
|
|
1147
|
+
onClickProp?.(event);
|
|
1148
|
+
if (event.defaultPrevented) return;
|
|
1149
|
+
store.setState("open", false);
|
|
1150
|
+
},
|
|
1151
|
+
[store, onClickProp]
|
|
1152
|
+
);
|
|
1153
|
+
return /* @__PURE__ */ jsx(Button, { type: "button", "data-slot": "tour-skip", variant: "outline", ...skipButtonProps, onClick, children: children ?? context.labels.skip });
|
|
1154
|
+
}
|
|
1155
|
+
function TourStepCounter(props) {
|
|
1156
|
+
const { format: formatProp, asChild, className, children, ...stepCounterProps } = props;
|
|
1157
|
+
const context = useTourContext(STEP_COUNTER_NAME);
|
|
1158
|
+
const format = formatProp ?? context.labels.stepCounter;
|
|
1159
|
+
const value = useStore((state) => state.value);
|
|
1160
|
+
const steps = useStore((state) => state.steps);
|
|
1161
|
+
const StepCounterPrimitive = asChild ? Slot : "div";
|
|
1162
|
+
return /* @__PURE__ */ jsx(
|
|
1163
|
+
StepCounterPrimitive,
|
|
1164
|
+
{
|
|
1165
|
+
"data-slot": "tour-step-counter",
|
|
1166
|
+
...stepCounterProps,
|
|
1167
|
+
className: cn("whitespace-nowrap text-sm text-muted-foreground", className),
|
|
1168
|
+
children: children ?? format(value + 1, steps.length)
|
|
1169
|
+
}
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
function TourFooter(props) {
|
|
1173
|
+
const { asChild, className, ref, ...footerProps } = props;
|
|
1174
|
+
const stepContext = useStepContext(FOOTER_NAME);
|
|
1175
|
+
const hasDefaultFooter = React.useContext(DefaultFooterContext);
|
|
1176
|
+
const context = useTourContext(FOOTER_NAME);
|
|
1177
|
+
const composedRef = useComposedRefs(ref, hasDefaultFooter ? void 0 : stepContext.onFooterChange);
|
|
1178
|
+
const FooterPrimitive = asChild ? Slot : "div";
|
|
1179
|
+
return /* @__PURE__ */ jsx(
|
|
1180
|
+
FooterPrimitive,
|
|
1181
|
+
{
|
|
1182
|
+
"data-slot": "tour-footer",
|
|
1183
|
+
dir: context.dir,
|
|
1184
|
+
...footerProps,
|
|
1185
|
+
ref: composedRef,
|
|
1186
|
+
className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)
|
|
1187
|
+
}
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
export {
|
|
1191
|
+
Tour,
|
|
1192
|
+
TourArrow,
|
|
1193
|
+
TourClose,
|
|
1194
|
+
TourDescription,
|
|
1195
|
+
TourFooter,
|
|
1196
|
+
TourHeader,
|
|
1197
|
+
TourNext,
|
|
1198
|
+
TourPortal,
|
|
1199
|
+
TourPrev,
|
|
1200
|
+
TourSkip,
|
|
1201
|
+
TourSpotlight,
|
|
1202
|
+
TourSpotlightRing,
|
|
1203
|
+
TourStep,
|
|
1204
|
+
TourStepCounter,
|
|
1205
|
+
TourTitle
|
|
1206
|
+
};
|
|
1207
|
+
//# sourceMappingURL=tour.js.map
|