@comitor/ui 2.0.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.
Files changed (131) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +14 -6
  3. package/dist/components/ui/action-bar-constants.d.ts +31 -0
  4. package/dist/components/ui/action-bar-constants.js +9 -0
  5. package/dist/components/ui/action-bar-constants.js.map +1 -0
  6. package/dist/components/ui/action-bar.d.ts +67 -0
  7. package/dist/components/ui/action-bar.js +522 -0
  8. package/dist/components/ui/action-bar.js.map +1 -0
  9. package/dist/components/ui/badge-overflow.d.ts +30 -0
  10. package/dist/components/ui/badge-overflow.js +179 -0
  11. package/dist/components/ui/badge-overflow.js.map +1 -0
  12. package/dist/components/ui/banner.d.ts +79 -0
  13. package/dist/components/ui/banner.js +518 -0
  14. package/dist/components/ui/banner.js.map +1 -0
  15. package/dist/components/ui/circular-progress.d.ts +50 -0
  16. package/dist/components/ui/circular-progress.js +266 -0
  17. package/dist/components/ui/circular-progress.js.map +1 -0
  18. package/dist/components/ui/cropper-constants.d.ts +34 -0
  19. package/dist/components/ui/cropper-constants.js +10 -0
  20. package/dist/components/ui/cropper-constants.js.map +1 -0
  21. package/dist/components/ui/cropper.d.ts +106 -0
  22. package/dist/components/ui/cropper.js +1269 -0
  23. package/dist/components/ui/cropper.js.map +1 -0
  24. package/dist/components/ui/drawer.d.ts +1 -1
  25. package/dist/components/ui/drawer.js +2 -2
  26. package/dist/components/ui/drawer.js.map +1 -1
  27. package/dist/components/ui/kanban-constants.d.ts +61 -0
  28. package/dist/components/ui/kanban-constants.js +21 -0
  29. package/dist/components/ui/kanban-constants.js.map +1 -0
  30. package/dist/components/ui/kanban.d.ts +72 -0
  31. package/dist/components/ui/kanban.js +787 -0
  32. package/dist/components/ui/kanban.js.map +1 -0
  33. package/dist/components/ui/key-value-constants.d.ts +33 -0
  34. package/dist/components/ui/key-value-constants.js +11 -0
  35. package/dist/components/ui/key-value-constants.js.map +1 -0
  36. package/dist/components/ui/key-value.d.ts +96 -0
  37. package/dist/components/ui/key-value.js +641 -0
  38. package/dist/components/ui/key-value.js.map +1 -0
  39. package/dist/components/ui/mask-input-constants.d.ts +99 -0
  40. package/dist/components/ui/mask-input-constants.js +601 -0
  41. package/dist/components/ui/mask-input-constants.js.map +1 -0
  42. package/dist/components/ui/mask-input.d.ts +36 -0
  43. package/dist/components/ui/mask-input.js +558 -0
  44. package/dist/components/ui/mask-input.js.map +1 -0
  45. package/dist/components/ui/mention.d.ts +31 -0
  46. package/dist/components/ui/mention.js +128 -0
  47. package/dist/components/ui/mention.js.map +1 -0
  48. package/dist/components/ui/phone-input-constants.d.ts +54 -0
  49. package/dist/components/ui/phone-input-constants.js +9 -0
  50. package/dist/components/ui/phone-input-constants.js.map +1 -0
  51. package/dist/components/ui/phone-input.d.ts +85 -0
  52. package/dist/components/ui/phone-input.js +743 -0
  53. package/dist/components/ui/phone-input.js.map +1 -0
  54. package/dist/components/ui/rating.d.ts +56 -0
  55. package/dist/components/ui/rating.js +646 -0
  56. package/dist/components/ui/rating.js.map +1 -0
  57. package/dist/components/ui/responsive-dialog.d.ts +35 -0
  58. package/dist/components/ui/responsive-dialog.js +193 -0
  59. package/dist/components/ui/responsive-dialog.js.map +1 -0
  60. package/dist/components/ui/scroll-spy.d.ts +42 -0
  61. package/dist/components/ui/scroll-spy.js +334 -0
  62. package/dist/components/ui/scroll-spy.js.map +1 -0
  63. package/dist/components/ui/stepper-constants.d.ts +29 -0
  64. package/dist/components/ui/stepper-constants.js +9 -0
  65. package/dist/components/ui/stepper-constants.js.map +1 -0
  66. package/dist/components/ui/stepper.d.ts +80 -0
  67. package/dist/components/ui/stepper.js +902 -0
  68. package/dist/components/ui/stepper.js.map +1 -0
  69. package/dist/components/ui/tags-input.d.ts +41 -0
  70. package/dist/components/ui/tags-input.js +127 -0
  71. package/dist/components/ui/tags-input.js.map +1 -0
  72. package/dist/components/ui/time-picker-constants.d.ts +48 -0
  73. package/dist/components/ui/time-picker-constants.js +14 -0
  74. package/dist/components/ui/time-picker-constants.js.map +1 -0
  75. package/dist/components/ui/time-picker.d.ts +115 -0
  76. package/dist/components/ui/time-picker.js +1620 -0
  77. package/dist/components/ui/time-picker.js.map +1 -0
  78. package/dist/components/ui/timeline.d.ts +32 -0
  79. package/dist/components/ui/timeline.js +559 -0
  80. package/dist/components/ui/timeline.js.map +1 -0
  81. package/dist/components/ui/tour-constants.d.ts +39 -0
  82. package/dist/components/ui/tour-constants.js +12 -0
  83. package/dist/components/ui/tour-constants.js.map +1 -0
  84. package/dist/components/ui/tour.d.ts +124 -0
  85. package/dist/components/ui/tour.js +1207 -0
  86. package/dist/components/ui/tour.js.map +1 -0
  87. package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
  88. package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
  89. package/dist/data-grid/data-grid-context-menu.js +1 -1
  90. package/dist/data-grid/data-grid-context-menu.js.map +1 -1
  91. package/dist/data-grid/data-grid-paste-dialog.js +1 -1
  92. package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
  93. package/dist/data-grid/data-grid-row.js +1 -1
  94. package/dist/data-grid/data-grid-row.js.map +1 -1
  95. package/dist/data-grid/data-grid-search.js +1 -1
  96. package/dist/data-grid/data-grid-search.js.map +1 -1
  97. package/dist/data-grid/data-grid.js +1 -1
  98. package/dist/data-grid/data-grid.js.map +1 -1
  99. package/dist/data-grid/sortable.js +1 -1
  100. package/dist/data-grid/sortable.js.map +1 -1
  101. package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
  102. package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
  103. package/dist/data-grid/use-data-grid.js +3 -3
  104. package/dist/data-grid/use-data-grid.js.map +1 -1
  105. package/dist/index.d.ts +31 -0
  106. package/dist/index.js +42 -0
  107. package/dist/index.js.map +1 -1
  108. package/dist/lib/compose-refs.js.map +1 -0
  109. package/dist/lib/use-as-ref.js.map +1 -0
  110. package/dist/lib/use-is-mobile.d.ts +10 -0
  111. package/dist/lib/use-is-mobile.js +21 -0
  112. package/dist/lib/use-is-mobile.js.map +1 -0
  113. package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
  114. package/dist/lib/use-lazy-ref.js.map +1 -0
  115. package/dist/lib/visually-hidden-input.d.ts +12 -0
  116. package/dist/lib/visually-hidden-input.js +105 -0
  117. package/dist/lib/visually-hidden-input.js.map +1 -0
  118. package/package.json +5 -1
  119. package/styles.css +19 -0
  120. package/dist/data-grid/compose-refs.js.map +0 -1
  121. package/dist/data-grid/use-as-ref.js.map +0 -1
  122. package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
  123. package/dist/data-grid/use-lazy-ref.js.map +0 -1
  124. /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
  125. /package/dist/{data-grid → lib}/compose-refs.js +0 -0
  126. /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
  127. /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
  128. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
  129. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
  130. /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
  131. /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