@inkra/react 0.8.0 → 0.9.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 (48) hide show
  1. package/README.md +89 -0
  2. package/dist/PencilIcon.d.ts +14 -1
  3. package/dist/PencilIcon.js +88 -52
  4. package/dist/guide/index.d.ts +77 -0
  5. package/dist/guide/index.js +379 -0
  6. package/dist/i18n/catalog.d.ts +1 -1
  7. package/dist/i18n/catalog.js +6 -0
  8. package/dist/image-viewer/index.d.ts +2 -0
  9. package/dist/image-viewer/index.js +28 -28
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +4 -0
  12. package/dist/infinite-scroll/gesture.d.ts +19 -0
  13. package/dist/infinite-scroll/gesture.js +218 -0
  14. package/dist/infinite-scroll/index.d.ts +55 -0
  15. package/dist/infinite-scroll/index.js +543 -0
  16. package/dist/infinite-scroll/model.d.ts +33 -0
  17. package/dist/infinite-scroll/model.js +168 -0
  18. package/dist/internal/iconFill.d.ts +16 -0
  19. package/dist/internal/iconFill.js +275 -0
  20. package/dist/inventory-grid/index.js +13 -3
  21. package/dist/inventory-grid/outline.d.ts +9 -0
  22. package/dist/inventory-grid/outline.js +54 -0
  23. package/dist/loading/index.d.ts +58 -0
  24. package/dist/loading/index.js +188 -0
  25. package/dist/loading/locales.d.ts +15 -0
  26. package/dist/loading/locales.js +34 -0
  27. package/dist/loading/model.d.ts +6 -0
  28. package/dist/loading/model.js +29 -0
  29. package/dist/masonry/index.d.ts +25 -0
  30. package/dist/masonry/index.js +227 -0
  31. package/dist/masonry/layout.d.ts +35 -0
  32. package/dist/masonry/layout.js +67 -0
  33. package/dist/number-ticker/RollingValue.d.ts +6 -0
  34. package/dist/number-ticker/RollingValue.js +10 -0
  35. package/dist/number-ticker/index.d.ts +4 -0
  36. package/dist/number-ticker/index.js +55 -7
  37. package/dist/number-ticker/rolling.d.ts +35 -0
  38. package/dist/number-ticker/rolling.js +87 -0
  39. package/dist/styles/guide.css +202 -0
  40. package/dist/styles/image-viewer.css +20 -0
  41. package/dist/styles/infinite-scroll.css +171 -0
  42. package/dist/styles/inventory-grid.css +20 -8
  43. package/dist/styles/loading.css +348 -0
  44. package/dist/styles/masonry.css +68 -0
  45. package/dist/styles/number-ticker.css +46 -0
  46. package/dist/styles.css +879 -8
  47. package/dist/toast/manager.d.ts +4 -4
  48. package/package.json +26 -1
@@ -0,0 +1,379 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as PopoverPrimitive from '@radix-ui/react-popover';
3
+ import { __iconData as skipIcon } from 'lucide-react/dist/esm/icons/skip-forward.mjs';
4
+ import { __iconData as closeIcon } from 'lucide-react/dist/esm/icons/x.mjs';
5
+ import { Children, Fragment, createContext, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
6
+ import { Button } from '../Button.js';
7
+ import { useInkraLocale } from '../i18n/index.js';
8
+ import { PencilIcon } from '../PencilIcon.js';
9
+ import { PencilSurface } from '../PencilSurface.js';
10
+ import { PencilText } from '../PencilText.js';
11
+ import { PortalThemeProvider, usePortalThemeProps, usePortalTriggerRef, } from '../overlay/themeBridge.js';
12
+ export function GuideStep(_props) {
13
+ return null;
14
+ }
15
+ GuideStep.displayName = 'Guide.Step';
16
+ const GuideTriggerContext = createContext(null);
17
+ function assignRef(ref, value) {
18
+ if (typeof ref === 'function')
19
+ ref(value);
20
+ else if (ref)
21
+ ref.current = value;
22
+ }
23
+ export const GuideTrigger = forwardRef(function GuideTrigger(props, forwardedRef) {
24
+ const context = useContext(GuideTriggerContext);
25
+ const portalRef = usePortalTriggerRef(forwardedRef);
26
+ const ref = useCallback((node) => {
27
+ portalRef(node);
28
+ if (node)
29
+ context?.setTrigger(node);
30
+ }, [context, portalRef]);
31
+ return _jsx(PopoverPrimitive.Trigger, { ...props, ref: ref });
32
+ });
33
+ function classes(...values) {
34
+ return values.filter(Boolean).join(' ');
35
+ }
36
+ function clampStep(step, count) {
37
+ if (count <= 0)
38
+ return 0;
39
+ const finite = Number.isFinite(step) ? Math.trunc(step) : 0;
40
+ return Math.min(count - 1, Math.max(0, finite));
41
+ }
42
+ function bounded(value, fallback, maximum) {
43
+ return Number.isFinite(value) ? Math.min(maximum, Math.max(0, value)) : fallback;
44
+ }
45
+ function resolveTarget(target) {
46
+ if (typeof document === 'undefined')
47
+ return null;
48
+ if (typeof target === 'string')
49
+ return document.querySelector(target);
50
+ if (typeof target === 'function')
51
+ return target();
52
+ if ('current' in target)
53
+ return target.current;
54
+ return target;
55
+ }
56
+ function isTextEntry(node) {
57
+ return node instanceof HTMLElement
58
+ && (node.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(node.tagName));
59
+ }
60
+ function formatProgress(template, current, total) {
61
+ return template.replace(/\{current\}/g, String(current)).replace(/\{total\}/g, String(total));
62
+ }
63
+ function collectGuideChildren(children) {
64
+ const steps = [];
65
+ const content = [];
66
+ const collect = (items) => Children.forEach(items, child => {
67
+ if (isValidElement(child) && child.type === Fragment) {
68
+ collect(child.props.children);
69
+ }
70
+ else if (isValidElement(child) && child.type === GuideStep) {
71
+ steps.push(child.props);
72
+ }
73
+ else {
74
+ content.push(child);
75
+ }
76
+ });
77
+ collect(children);
78
+ return { steps, content };
79
+ }
80
+ const closePaths = closeIcon.node.map(([, attributes]) => ({ d: attributes.d }));
81
+ const skipPaths = skipIcon.node.map(([, attributes]) => ({ d: attributes.d }));
82
+ const cornerPlacements = ['top-start', 'top-end', 'bottom-start', 'bottom-end'];
83
+ function focusableElements(container) {
84
+ if (!container)
85
+ return [];
86
+ return [...container.querySelectorAll('button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])')]
87
+ .filter(element => !element.hidden && element.getAttribute('aria-hidden') !== 'true');
88
+ }
89
+ function cornerRect(placement, width, height, direction) {
90
+ const inset = 16;
91
+ const start = direction === 'rtl' ? window.innerWidth - inset - width : inset;
92
+ const end = direction === 'rtl' ? inset : window.innerWidth - inset - width;
93
+ return new DOMRect(placement.endsWith('start') ? start : end, placement.startsWith('top') ? inset : window.innerHeight - inset - height, width, height);
94
+ }
95
+ function overlaps(left, right, gap = 8) {
96
+ return left.left < right.right + gap && left.right > right.left - gap
97
+ && left.top < right.bottom + gap && left.bottom > right.top - gap;
98
+ }
99
+ function GuideLayer({ activeStep, activeIndex, total, className, closeOnEscape, highlightPadding, highlightRadius, labels, portalContainer, scrollIntoView, showClose, showSkip, skipAppearance, skipPlacement, onClose, onEscape, onFinish, onNext, onPrevious, onSkip, }) {
100
+ const titleId = useId();
101
+ const descriptionId = useId();
102
+ const contentRef = useRef(null);
103
+ const skipRef = useRef(null);
104
+ const [resolvedSkipPlacement, setResolvedSkipPlacement] = useState(skipPlacement);
105
+ const [target, setTarget] = useState(null);
106
+ const [targetRect, setTargetRect] = useState(null);
107
+ const [viewportWidth, setViewportWidth] = useState(0);
108
+ const fallbackAnchor = useRef({
109
+ getBoundingClientRect: () => {
110
+ const width = typeof window === 'undefined' ? 0 : window.innerWidth;
111
+ const height = typeof window === 'undefined' ? 0 : window.innerHeight;
112
+ return new DOMRect(width / 2, Math.max(24, height * 0.38), 1, 1);
113
+ },
114
+ });
115
+ const anchorRef = useRef(fallbackAnchor.current);
116
+ anchorRef.current = target ?? fallbackAnchor.current;
117
+ const measure = useCallback(() => {
118
+ setViewportWidth(current => current === window.innerWidth ? current : window.innerWidth);
119
+ const nextTarget = resolveTarget(activeStep.target);
120
+ setTarget(current => current === nextTarget ? current : nextTarget);
121
+ const nextRect = nextTarget?.getBoundingClientRect() ?? null;
122
+ setTargetRect(current => {
123
+ if (!current && !nextRect)
124
+ return current;
125
+ if (current && nextRect
126
+ && current.x === nextRect.x && current.y === nextRect.y
127
+ && current.width === nextRect.width && current.height === nextRect.height)
128
+ return current;
129
+ return nextRect;
130
+ });
131
+ }, [activeStep.target]);
132
+ useEffect(() => {
133
+ let frame = 0;
134
+ let secondFrame = 0;
135
+ const initialTarget = resolveTarget(activeStep.target);
136
+ if (initialTarget && scrollIntoView) {
137
+ const options = scrollIntoView === true
138
+ ? { block: 'center', inline: 'nearest', behavior: 'smooth' }
139
+ : scrollIntoView;
140
+ if (typeof window === 'undefined' || !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
141
+ initialTarget.scrollIntoView(options);
142
+ }
143
+ else {
144
+ initialTarget.scrollIntoView({ ...options, behavior: 'auto' });
145
+ }
146
+ }
147
+ measure();
148
+ frame = requestAnimationFrame(() => {
149
+ measure();
150
+ secondFrame = requestAnimationFrame(measure);
151
+ });
152
+ const observer = typeof MutationObserver === 'undefined' ? null : new MutationObserver(measure);
153
+ observer?.observe(document.body, {
154
+ childList: true,
155
+ subtree: true,
156
+ attributes: true,
157
+ attributeFilter: ['id', 'class', 'hidden'],
158
+ });
159
+ window.addEventListener('resize', measure);
160
+ window.addEventListener('scroll', measure, true);
161
+ return () => {
162
+ cancelAnimationFrame(frame);
163
+ cancelAnimationFrame(secondFrame);
164
+ observer?.disconnect();
165
+ window.removeEventListener('resize', measure);
166
+ window.removeEventListener('scroll', measure, true);
167
+ };
168
+ }, [activeIndex, activeStep.target, measure, scrollIntoView]);
169
+ useEffect(() => {
170
+ if (!target || typeof ResizeObserver === 'undefined')
171
+ return undefined;
172
+ const observer = new ResizeObserver(measure);
173
+ observer.observe(target);
174
+ return () => observer.disconnect();
175
+ }, [measure, target]);
176
+ useEffect(() => {
177
+ if (!showSkip || skipPlacement === 'panel') {
178
+ setResolvedSkipPlacement(skipPlacement);
179
+ return undefined;
180
+ }
181
+ let frame = 0;
182
+ const resolvePlacement = () => {
183
+ cancelAnimationFrame(frame);
184
+ frame = requestAnimationFrame(() => {
185
+ const content = contentRef.current;
186
+ const skip = skipRef.current;
187
+ if (!content)
188
+ return;
189
+ const contentRect = content.getBoundingClientRect();
190
+ const skipRect = skip?.getBoundingClientRect();
191
+ const direction = getComputedStyle(content).direction === 'rtl' ? 'rtl' : 'ltr';
192
+ const preferred = skipPlacement;
193
+ const alternatives = [
194
+ preferred,
195
+ preferred.startsWith('top') ? preferred.replace('top', 'bottom') : preferred.replace('bottom', 'top'),
196
+ preferred.endsWith('start') ? preferred.replace('start', 'end') : preferred.replace('end', 'start'),
197
+ ...cornerPlacements,
198
+ ];
199
+ const width = skipRect?.width || (skipAppearance === 'icon' ? 44 : 128);
200
+ const height = skipRect?.height || 44;
201
+ const next = alternatives.find(placement => !overlaps(cornerRect(placement, width, height, direction), contentRect)) ?? 'panel';
202
+ setResolvedSkipPlacement(current => current === next ? current : next);
203
+ });
204
+ };
205
+ resolvePlacement();
206
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(resolvePlacement);
207
+ if (contentRef.current)
208
+ observer?.observe(contentRef.current);
209
+ if (skipRef.current)
210
+ observer?.observe(skipRef.current);
211
+ window.addEventListener('resize', resolvePlacement);
212
+ window.addEventListener('scroll', resolvePlacement, true);
213
+ return () => {
214
+ cancelAnimationFrame(frame);
215
+ observer?.disconnect();
216
+ window.removeEventListener('resize', resolvePlacement);
217
+ window.removeEventListener('scroll', resolvePlacement, true);
218
+ };
219
+ }, [activeIndex, showSkip, skipAppearance, skipPlacement]);
220
+ const themedProps = usePortalThemeProps({});
221
+ const paddedRect = targetRect ? {
222
+ left: Math.max(0, targetRect.left - highlightPadding),
223
+ top: Math.max(0, targetRect.top - highlightPadding),
224
+ width: Math.max(1, Math.min(window.innerWidth, targetRect.right + highlightPadding) - Math.max(0, targetRect.left - highlightPadding)),
225
+ height: Math.max(1, Math.min(window.innerHeight, targetRect.bottom + highlightPadding) - Math.max(0, targetRect.top - highlightPadding)),
226
+ } : null;
227
+ const description = activeStep.description ?? activeStep.children;
228
+ const last = activeIndex === total - 1;
229
+ const progress = formatProgress(labels.progress, activeIndex + 1, total);
230
+ const collisionPadding = activeStep.collisionPadding ?? 16;
231
+ const sideOffset = activeStep.sideOffset ?? 12;
232
+ const preferredSide = activeStep.side ?? 'bottom';
233
+ const expectedWidth = viewportWidth <= 480
234
+ ? Math.max(1, viewportWidth - 16)
235
+ : Math.min(352, Math.max(1, viewportWidth - 24));
236
+ const side = targetRect && (preferredSide === 'left' || preferredSide === 'right')
237
+ && Math.max(targetRect.left - collisionPadding - sideOffset, viewportWidth - targetRect.right - collisionPadding - sideOffset) < expectedWidth
238
+ ? 'bottom'
239
+ : preferredSide;
240
+ const handleKeyDown = (event) => {
241
+ if (event.key === 'Tab') {
242
+ const focusables = focusableElements(contentRef.current);
243
+ const first = focusables[0];
244
+ const lastFocusable = focusables[focusables.length - 1];
245
+ const cornerSkip = showSkip && resolvedSkipPlacement !== 'panel';
246
+ if (!event.shiftKey && event.target === lastFocusable) {
247
+ event.preventDefault();
248
+ (cornerSkip ? skipRef.current : first ?? contentRef.current)?.focus();
249
+ return;
250
+ }
251
+ if (event.shiftKey && (event.target === first || event.target === contentRef.current)) {
252
+ event.preventDefault();
253
+ (cornerSkip ? skipRef.current : lastFocusable ?? contentRef.current)?.focus();
254
+ return;
255
+ }
256
+ }
257
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || isTextEntry(event.target))
258
+ return;
259
+ const direction = getComputedStyle(event.currentTarget).direction;
260
+ const nextKey = direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
261
+ const previousKey = direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
262
+ if (event.key === nextKey) {
263
+ event.preventDefault();
264
+ last ? onFinish() : onNext();
265
+ }
266
+ else if (event.key === previousKey && activeIndex > 0) {
267
+ event.preventDefault();
268
+ onPrevious();
269
+ }
270
+ };
271
+ const handleSkipKeyDown = (event) => {
272
+ if (event.key !== 'Tab')
273
+ return;
274
+ const focusables = focusableElements(contentRef.current);
275
+ const destination = event.shiftKey ? focusables[focusables.length - 1] ?? contentRef.current : focusables[0] ?? contentRef.current;
276
+ event.preventDefault();
277
+ destination?.focus();
278
+ };
279
+ const skipAction = (corner) => _jsx(Button, { "aria-label": labels.skip, className: corner ? 'inkra-guide__skip' : 'inkra-guide__skip-panel', "data-appearance": skipAppearance, "data-placement": corner ? resolvedSkipPlacement : 'panel', "data-tooltip": corner && skipAppearance === 'icon' ? labels.skip : undefined, onClick: onSkip, onKeyDown: corner ? handleSkipKeyDown : undefined, ref: corner ? skipRef : undefined, size: skipAppearance === 'icon' ? 'icon-xs' : 'sm', title: corner && skipAppearance === 'icon' ? labels.skip : undefined, variant: "ghost", children: skipAppearance === 'icon'
280
+ ? _jsx(PencilIcon, { decorative: true, paths: skipPaths, size: 18, sourceSize: 24 })
281
+ : labels.skip });
282
+ return (_jsxs(_Fragment, { children: [_jsx(PopoverPrimitive.Anchor, { virtualRef: anchorRef }), _jsx(PopoverPrimitive.Portal, { container: portalContainer ?? undefined, children: paddedRect ? (_jsx(PencilSurface, { ...themedProps, "aria-hidden": "true", className: "inkra-guide__spotlight", radius: highlightRadius, seed: `inkra-guide-highlight-${activeStep.id ?? activeIndex}`, style: {
283
+ '--inkra-guide-highlight-radius': `${highlightRadius}px`,
284
+ height: paddedRect.height,
285
+ left: paddedRect.left,
286
+ top: paddedRect.top,
287
+ width: paddedRect.width,
288
+ }, variant: "outline" })) : _jsx("div", { ...themedProps, "aria-hidden": "true", className: "inkra-guide__backdrop" }) }), showSkip && resolvedSkipPlacement !== 'panel' ? (_jsx(PopoverPrimitive.Portal, { container: portalContainer ?? undefined, children: _jsx("div", { ...themedProps, className: "inkra-guide__skip-layer", children: skipAction(true) }) })) : null, _jsx(PopoverPrimitive.Portal, { container: portalContainer ?? undefined, children: _jsx(PopoverPrimitive.Content, { ...themedProps, ref: contentRef, role: "dialog", "aria-modal": "false", "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : undefined, align: activeStep.align ?? 'center', className: classes('inkra-guide__content', className), collisionPadding: collisionPadding, "data-target-missing": !target || undefined, onCloseAutoFocus: event => event.preventDefault(), onEscapeKeyDown: event => {
289
+ event.preventDefault();
290
+ if (closeOnEscape)
291
+ onEscape();
292
+ }, onFocusOutside: event => event.preventDefault(), onInteractOutside: event => event.preventDefault(), onKeyDown: handleKeyDown, onOpenAutoFocus: event => {
293
+ event.preventDefault();
294
+ requestAnimationFrame(() => contentRef.current?.focus({ preventScroll: true }));
295
+ }, side: side, sideOffset: sideOffset, tabIndex: -1, children: _jsx(PencilSurface, { className: "inkra-guide__surface", radius: 8, seed: `inkra-guide-panel-${activeStep.id ?? activeIndex}`, variant: "paper", children: _jsxs("div", { className: "inkra-guide__panel", children: [_jsxs("header", { className: "inkra-guide__header", children: [_jsxs("div", { className: "inkra-guide__heading", children: [_jsx("span", { className: "inkra-guide__progress", children: _jsx(PencilText, { children: progress }) }), _jsx("h2", { className: "inkra-guide__title", id: titleId, children: _jsx(PencilText, { children: activeStep.title }) })] }), showClose ? (_jsx(Button, { "aria-label": labels.close, className: "inkra-guide__close", onClick: onClose, size: "icon", variant: "ghost", children: _jsx(PencilIcon, { decorative: true, paths: closePaths, size: 24, sourceSize: 24 }) })) : null] }), description ? (_jsx("div", { className: "inkra-guide__description", id: descriptionId, children: typeof description === 'string' ? _jsx(PencilText, { children: description }) : description })) : null, _jsxs("footer", { className: "inkra-guide__footer", children: [showSkip && resolvedSkipPlacement === 'panel' ? skipAction(false) : _jsx("span", {}), _jsxs("div", { className: "inkra-guide__navigation", children: [_jsx(Button, { disabled: activeIndex === 0, onClick: onPrevious, variant: "secondary", children: labels.previous }), _jsx(Button, { onClick: last ? onFinish : onNext, children: last ? labels.finish : labels.next })] })] })] }) }) }) })] }));
296
+ }
297
+ export function GuideRoot({ children, open, defaultOpen = false, onOpenChange, step, defaultStep = 0, onStepChange, onFinish, onSkip, showSkip = true, skipPlacement = 'top-end', skipAppearance = 'icon', showClose = true, closeOnEscape = true, scrollIntoView = true, restoreFocus = true, highlightPadding, highlightRadius, locale, labels, portalContainer, className, }) {
298
+ const { t } = useInkraLocale(locale);
299
+ const { steps, content } = useMemo(() => collectGuideChildren(children), [children]);
300
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
301
+ const [uncontrolledStep, setUncontrolledStep] = useState(() => clampStep(defaultStep, steps.length));
302
+ const triggerRef = useRef(null);
303
+ const previousFocusRef = useRef(null);
304
+ const wasVisibleOpenRef = useRef(false);
305
+ const controlledOpen = open !== undefined;
306
+ const controlledStep = step !== undefined;
307
+ const currentOpen = controlledOpen ? open : uncontrolledOpen;
308
+ const activeIndex = clampStep(controlledStep ? step : uncontrolledStep, steps.length);
309
+ const activeStep = steps[activeIndex];
310
+ const visibleOpen = Boolean(currentOpen && activeStep);
311
+ const resolvedLabels = useMemo(() => ({
312
+ previous: labels?.previous ?? t('guide.previous'),
313
+ next: labels?.next ?? t('guide.next'),
314
+ finish: labels?.finish ?? t('guide.finish'),
315
+ skip: labels?.skip ?? t('guide.skip'),
316
+ close: labels?.close ?? t('guide.close'),
317
+ progress: labels?.progress ?? t('guide.progress'),
318
+ }), [labels, t]);
319
+ useEffect(() => {
320
+ if (visibleOpen)
321
+ previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
322
+ }, [visibleOpen]);
323
+ useEffect(() => {
324
+ const wasVisibleOpen = wasVisibleOpenRef.current;
325
+ wasVisibleOpenRef.current = visibleOpen;
326
+ if (!wasVisibleOpen || visibleOpen || !restoreFocus)
327
+ return undefined;
328
+ const frame = requestAnimationFrame(() => {
329
+ const previousFocus = previousFocusRef.current;
330
+ const trigger = triggerRef.current;
331
+ const destination = trigger?.isConnected ? trigger : previousFocus?.isConnected ? previousFocus : null;
332
+ destination?.focus({ preventScroll: true });
333
+ });
334
+ return () => cancelAnimationFrame(frame);
335
+ }, [restoreFocus, visibleOpen]);
336
+ useEffect(() => {
337
+ if (!controlledStep && uncontrolledStep !== activeIndex)
338
+ setUncontrolledStep(activeIndex);
339
+ }, [activeIndex, controlledStep, uncontrolledStep]);
340
+ const requestOpen = useCallback((nextOpen, reason) => {
341
+ if (!controlledOpen)
342
+ setUncontrolledOpen(nextOpen);
343
+ onOpenChange?.(nextOpen, { reason, step: activeIndex, stepId: activeStep?.id });
344
+ }, [activeIndex, activeStep?.id, controlledOpen, onOpenChange]);
345
+ const requestStep = useCallback((nextStep, reason) => {
346
+ const next = clampStep(nextStep, steps.length);
347
+ if (next === activeIndex)
348
+ return;
349
+ if (!controlledStep)
350
+ setUncontrolledStep(next);
351
+ onStepChange?.(next, { reason, previousStep: activeIndex, stepId: steps[next]?.id });
352
+ }, [activeIndex, controlledStep, onStepChange, steps]);
353
+ const resetUncontrolledStep = useCallback(() => {
354
+ if (!controlledStep)
355
+ setUncontrolledStep(clampStep(defaultStep, steps.length));
356
+ }, [controlledStep, defaultStep, steps.length]);
357
+ const finish = useCallback(() => {
358
+ onFinish?.();
359
+ requestOpen(false, 'finish');
360
+ resetUncontrolledStep();
361
+ }, [onFinish, requestOpen, resetUncontrolledStep]);
362
+ const skip = useCallback(() => {
363
+ onSkip?.();
364
+ requestOpen(false, 'skip');
365
+ resetUncontrolledStep();
366
+ }, [onSkip, requestOpen, resetUncontrolledStep]);
367
+ const triggerContext = useMemo(() => ({
368
+ setTrigger: (node) => {
369
+ if (node)
370
+ triggerRef.current = node;
371
+ },
372
+ }), []);
373
+ return (_jsx(PortalThemeProvider, { children: _jsx(GuideTriggerContext.Provider, { value: triggerContext, children: _jsxs(PopoverPrimitive.Root, { modal: false, open: visibleOpen, onOpenChange: nextOpen => requestOpen(nextOpen, 'trigger'), children: [content, visibleOpen && activeStep ? (_jsx(GuideLayer, { activeIndex: activeIndex, activeStep: activeStep, className: className, closeOnEscape: closeOnEscape, highlightPadding: bounded(highlightPadding, 8, 48), highlightRadius: bounded(highlightRadius, 8, 32), labels: resolvedLabels, onClose: () => requestOpen(false, 'close'), onEscape: () => requestOpen(false, 'escape'), onFinish: finish, onNext: () => requestStep(activeIndex + 1, 'next'), onPrevious: () => requestStep(activeIndex - 1, 'previous'), onSkip: skip, portalContainer: portalContainer, scrollIntoView: scrollIntoView, showClose: showClose, showSkip: showSkip, skipAppearance: skipAppearance, skipPlacement: skipPlacement, total: steps.length })) : null] }) }) }));
374
+ }
375
+ export const Guide = /* @__PURE__ */ Object.freeze({
376
+ Root: GuideRoot,
377
+ Trigger: GuideTrigger,
378
+ Step: GuideStep,
379
+ });