mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(tour): fix tour auto-start logic and standardize selectors (#3751)
* improvement(tour): fix tour auto-start logic and standardize selectors * fix(tour): address PR review comments - Move autoStartAttempted.add() inside timer callback to prevent blocking auto-start when tour first mounts while disabled - Memoize setJoyrideRef with useCallback to prevent ref churn - Remove unused joyrideRef
This commit is contained in:
@@ -2,7 +2,7 @@ import type { Step } from 'react-joyride'
|
||||
|
||||
export const navTourSteps: Step[] = [
|
||||
{
|
||||
target: '[data-item-id="home"]',
|
||||
target: '[data-tour="nav-home"]',
|
||||
title: 'Home',
|
||||
content:
|
||||
'Your starting point. Describe what you want to build in plain language or pick a template to get started.',
|
||||
@@ -11,7 +11,7 @@ export const navTourSteps: Step[] = [
|
||||
spotlightPadding: 0,
|
||||
},
|
||||
{
|
||||
target: '[data-item-id="search"]',
|
||||
target: '[data-tour="nav-search"]',
|
||||
title: 'Search',
|
||||
content: 'Quickly find workflows, blocks, and tools. Use Cmd+K to open it from anywhere.',
|
||||
placement: 'right',
|
||||
@@ -19,7 +19,7 @@ export const navTourSteps: Step[] = [
|
||||
spotlightPadding: 0,
|
||||
},
|
||||
{
|
||||
target: '[data-item-id="tables"]',
|
||||
target: '[data-tour="nav-tables"]',
|
||||
title: 'Tables',
|
||||
content:
|
||||
'Store and query structured data. Your workflows can read and write to tables directly.',
|
||||
@@ -27,14 +27,14 @@ export const navTourSteps: Step[] = [
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-item-id="files"]',
|
||||
target: '[data-tour="nav-files"]',
|
||||
title: 'Files',
|
||||
content: 'Upload and manage files that your workflows can process, transform, or reference.',
|
||||
placement: 'right',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-item-id="knowledge-base"]',
|
||||
target: '[data-tour="nav-knowledge-base"]',
|
||||
title: 'Knowledge Base',
|
||||
content:
|
||||
'Build knowledge bases from your documents. Set up connectors to give your agents realtime access to your data sources from sources like Notion, Drive, Slack, Confluence, and more.',
|
||||
@@ -42,7 +42,7 @@ export const navTourSteps: Step[] = [
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-item-id="scheduled-tasks"]',
|
||||
target: '[data-tour="nav-scheduled-tasks"]',
|
||||
title: 'Scheduled Tasks',
|
||||
content:
|
||||
'View and manage background tasks. Set up new tasks, or view the tasks the Mothership is monitoring for upcoming or past executions.',
|
||||
@@ -50,7 +50,7 @@ export const navTourSteps: Step[] = [
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-item-id="logs"]',
|
||||
target: '[data-tour="nav-logs"]',
|
||||
title: 'Logs',
|
||||
content:
|
||||
'Monitor every workflow execution. See inputs, outputs, errors, and timing for each run. View analytics on performance and costs, filter previous runs, and view snapshots of the workflow at the time of execution.',
|
||||
@@ -58,7 +58,7 @@ export const navTourSteps: Step[] = [
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '.tasks-section',
|
||||
target: '[data-tour="nav-tasks"]',
|
||||
title: 'Tasks',
|
||||
content:
|
||||
'Tasks that work for you. Mothership can create, edit, and delete resource throughout the platform. It can also perform actions on your behalf, like sending emails, creating tasks, and more.',
|
||||
@@ -66,7 +66,7 @@ export const navTourSteps: Step[] = [
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '.workflows-section',
|
||||
target: '[data-tour="nav-workflows"]',
|
||||
title: 'Workflows',
|
||||
content:
|
||||
'All your workflows live here. Create new ones with the + button and organize them into folders. Deploy your workflows as API, webhook, schedule, or chat widget. Then hit Run to test it out.',
|
||||
|
||||
@@ -72,12 +72,22 @@ export function TourTooltipAdapter({
|
||||
}
|
||||
}, [step])
|
||||
|
||||
const refCallback = useCallback(
|
||||
/**
|
||||
* Forwards the Joyride tooltip ref safely, handling both
|
||||
* callback refs and RefObject refs from the library.
|
||||
* Memoized to prevent ref churn (null → node cycling) on re-renders.
|
||||
*/
|
||||
const setJoyrideRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
if (tooltipProps.ref) {
|
||||
;(tooltipProps.ref as React.RefCallback<HTMLDivElement>)(node)
|
||||
const { ref } = tooltipProps
|
||||
if (!ref) return
|
||||
if (typeof ref === 'function') {
|
||||
ref(node)
|
||||
} else {
|
||||
;(ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[tooltipProps.ref]
|
||||
)
|
||||
|
||||
@@ -86,7 +96,7 @@ export function TourTooltipAdapter({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={refCallback}
|
||||
ref={setJoyrideRef}
|
||||
role={tooltipProps.role}
|
||||
aria-modal={tooltipProps['aria-modal']}
|
||||
style={{ position: 'absolute', opacity: 0, pointerEvents: 'none', width: 0, height: 0 }}
|
||||
|
||||
@@ -65,6 +65,13 @@ function clearTourCompletion(storageKey: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks which tours have already attempted auto-start in this page session.
|
||||
* Module-level so it survives component remounts (e.g. navigating between
|
||||
* workflows remounts WorkflowTour), while still resetting on full page reload.
|
||||
*/
|
||||
const autoStartAttempted = new Set<string>()
|
||||
|
||||
/**
|
||||
* Shared hook for managing product tour state with smooth transitions.
|
||||
*
|
||||
@@ -87,16 +94,51 @@ export function useTour({
|
||||
const [isTooltipVisible, setIsTooltipVisible] = useState(true)
|
||||
const [isEntrance, setIsEntrance] = useState(true)
|
||||
|
||||
const hasAutoStarted = useRef(false)
|
||||
const disabledRef = useRef(disabled)
|
||||
const retriggerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const transitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const rafRef = useRef<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
disabledRef.current = disabled
|
||||
}, [disabled])
|
||||
|
||||
/**
|
||||
* Schedules a two-frame rAF to reveal the tooltip after the browser
|
||||
* finishes repositioning. Stores the outer frame ID in `rafRef` so
|
||||
* it can be cancelled on unmount or when the tour is interrupted.
|
||||
*/
|
||||
const scheduleReveal = useCallback(() => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current)
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
rafRef.current = null
|
||||
setIsTooltipVisible(true)
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
/** Cancels any pending transition timer and rAF reveal */
|
||||
const cancelPendingTransitions = useCallback(() => {
|
||||
if (transitionTimerRef.current) {
|
||||
clearTimeout(transitionTimerRef.current)
|
||||
transitionTimerRef.current = null
|
||||
}
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current)
|
||||
rafRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const stopTour = useCallback(() => {
|
||||
cancelPendingTransitions()
|
||||
setRun(false)
|
||||
setIsTooltipVisible(true)
|
||||
setIsEntrance(true)
|
||||
markTourCompleted(storageKey)
|
||||
}, [storageKey])
|
||||
}, [storageKey, cancelPendingTransitions])
|
||||
|
||||
/** Transition to a new step with a coordinated fade-out/fade-in */
|
||||
const transitionToStep = useCallback(
|
||||
@@ -106,65 +148,48 @@ export function useTour({
|
||||
return
|
||||
}
|
||||
|
||||
/** Hide tooltip during transition */
|
||||
setIsTooltipVisible(false)
|
||||
|
||||
if (transitionTimerRef.current) {
|
||||
clearTimeout(transitionTimerRef.current)
|
||||
}
|
||||
cancelPendingTransitions()
|
||||
|
||||
transitionTimerRef.current = setTimeout(() => {
|
||||
transitionTimerRef.current = null
|
||||
setStepIndex(newIndex)
|
||||
setIsEntrance(false)
|
||||
|
||||
/**
|
||||
* Wait for the browser to process the Radix Popover repositioning
|
||||
* before showing the tooltip at the new position.
|
||||
*/
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setIsTooltipVisible(true)
|
||||
})
|
||||
})
|
||||
scheduleReveal()
|
||||
}, FADE_OUT_MS)
|
||||
},
|
||||
[steps.length, stopTour]
|
||||
[steps.length, stopTour, cancelPendingTransitions, scheduleReveal]
|
||||
)
|
||||
|
||||
/** Stop the tour when disabled becomes true (e.g. navigating away from the relevant page) */
|
||||
useEffect(() => {
|
||||
if (disabled && run) {
|
||||
cancelPendingTransitions()
|
||||
setRun(false)
|
||||
setIsTooltipVisible(true)
|
||||
setIsEntrance(true)
|
||||
logger.info(`${tourName} paused — disabled became true`)
|
||||
}
|
||||
}, [disabled, run, tourName])
|
||||
}, [disabled, run, tourName, cancelPendingTransitions])
|
||||
|
||||
/** Auto-start on first visit */
|
||||
/** Auto-start on first visit (once per page session per tour) */
|
||||
useEffect(() => {
|
||||
if (disabled || hasAutoStarted.current) return
|
||||
if (disabled || autoStartAttempted.has(storageKey) || isTourCompleted(storageKey)) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
hasAutoStarted.current = true
|
||||
if (!isTourCompleted(storageKey)) {
|
||||
setStepIndex(0)
|
||||
setIsEntrance(true)
|
||||
setIsTooltipVisible(false)
|
||||
setRun(true)
|
||||
logger.info(`Auto-starting ${tourName}`)
|
||||
if (disabledRef.current) return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setIsTooltipVisible(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
autoStartAttempted.add(storageKey)
|
||||
setStepIndex(0)
|
||||
setIsEntrance(true)
|
||||
setIsTooltipVisible(false)
|
||||
setRun(true)
|
||||
logger.info(`Auto-starting ${tourName}`)
|
||||
scheduleReveal()
|
||||
}, autoStartDelay)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [storageKey, autoStartDelay, tourName, disabled])
|
||||
}, [disabled, storageKey, autoStartDelay, tourName, scheduleReveal])
|
||||
|
||||
/** Listen for manual trigger events */
|
||||
useEffect(() => {
|
||||
@@ -179,11 +204,6 @@ export function useTour({
|
||||
clearTimeout(retriggerTimerRef.current)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start with the tooltip hidden so Joyride can mount, find the
|
||||
* target element, and position its overlay/spotlight before the
|
||||
* tooltip card appears.
|
||||
*/
|
||||
retriggerTimerRef.current = setTimeout(() => {
|
||||
retriggerTimerRef.current = null
|
||||
setStepIndex(0)
|
||||
@@ -191,12 +211,7 @@ export function useTour({
|
||||
setIsTooltipVisible(false)
|
||||
setRun(true)
|
||||
logger.info(`${tourName} triggered via event`)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setIsTooltipVisible(true)
|
||||
})
|
||||
})
|
||||
scheduleReveal()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
@@ -207,15 +222,17 @@ export function useTour({
|
||||
clearTimeout(retriggerTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [triggerEvent, resettable, storageKey, tourName])
|
||||
}, [triggerEvent, resettable, storageKey, tourName, scheduleReveal])
|
||||
|
||||
/** Clean up all pending async work on unmount */
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (transitionTimerRef.current) {
|
||||
clearTimeout(transitionTimerRef.current)
|
||||
cancelPendingTransitions()
|
||||
if (retriggerTimerRef.current) {
|
||||
clearTimeout(retriggerTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [cancelPendingTransitions])
|
||||
|
||||
const handleCallback = useCallback(
|
||||
(data: CallBackProps) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ export const workflowTourSteps: Step[] = [
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tab-button="copilot"]',
|
||||
target: '[data-tour="tab-copilot"]',
|
||||
title: 'AI Copilot',
|
||||
content:
|
||||
'Build and debug workflows using natural language. Describe what you want and Copilot creates the blocks for you.',
|
||||
@@ -19,7 +19,7 @@ export const workflowTourSteps: Step[] = [
|
||||
spotlightPadding: 0,
|
||||
},
|
||||
{
|
||||
target: '[data-tab-button="toolbar"]',
|
||||
target: '[data-tour="tab-toolbar"]',
|
||||
title: 'Block Library',
|
||||
content:
|
||||
'Browse all available blocks and triggers. Drag them onto the canvas to build your workflow step by step.',
|
||||
@@ -28,7 +28,7 @@ export const workflowTourSteps: Step[] = [
|
||||
spotlightPadding: 0,
|
||||
},
|
||||
{
|
||||
target: '[data-tab-button="editor"]',
|
||||
target: '[data-tour="tab-editor"]',
|
||||
title: 'Block Editor',
|
||||
content:
|
||||
'Click any block on the canvas to configure it here. Set inputs, credentials, and fine-tune behavior.',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Camera, Check, Pencil } from 'lucide-react'
|
||||
import { Camera, Check, Info, Pencil } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
@@ -376,20 +376,22 @@ export function General() {
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Label htmlFor='auto-connect' className='cursor-help'>
|
||||
Auto-connect on drop
|
||||
</Label>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom' align='start'>
|
||||
<p>Automatically connect blocks when dropped near each other</p>
|
||||
<Tooltip.Preview
|
||||
src='/tooltips/auto-connect-on-drop.mp4'
|
||||
alt='Auto-connect on drop example'
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Label htmlFor='auto-connect'>Auto-connect on drop</Label>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Info className='h-[14px] w-[14px] cursor-help text-[var(--text-muted)]' />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom' align='start'>
|
||||
<p>Automatically connect blocks when dropped near each other</p>
|
||||
<Tooltip.Preview
|
||||
src='/tooltips/auto-connect-on-drop.mp4'
|
||||
alt='Auto-connect on drop example'
|
||||
loop={false}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<Switch
|
||||
id='auto-connect'
|
||||
checked={settings?.autoConnect ?? true}
|
||||
@@ -398,20 +400,21 @@ export function General() {
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Label htmlFor='error-notifications' className='cursor-help'>
|
||||
Canvas error notifications
|
||||
</Label>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom' align='start'>
|
||||
<p>Show error popups on blocks when a workflow run fails</p>
|
||||
<Tooltip.Preview
|
||||
src='/tooltips/canvas-error-notification.mp4'
|
||||
alt='Canvas error notification example'
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Label htmlFor='error-notifications'>Canvas error notifications</Label>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Info className='h-[14px] w-[14px] cursor-help text-[var(--text-muted)]' />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom' align='start'>
|
||||
<p>Show error popups on blocks when a workflow run fails</p>
|
||||
<Tooltip.Preview
|
||||
src='/tooltips/canvas-error-notification.mp4'
|
||||
alt='Canvas error notification example'
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<Switch
|
||||
id='error-notifications'
|
||||
checked={settings?.errorNotificationsEnabled ?? true}
|
||||
|
||||
@@ -692,6 +692,7 @@ export const Panel = memo(function Panel() {
|
||||
variant={_hasHydrated && activeTab === 'copilot' ? 'active' : 'ghost'}
|
||||
onClick={() => handleTabClick('copilot')}
|
||||
data-tab-button='copilot'
|
||||
data-tour='tab-copilot'
|
||||
>
|
||||
Copilot
|
||||
</Button>
|
||||
@@ -705,6 +706,7 @@ export const Panel = memo(function Panel() {
|
||||
variant={_hasHydrated && activeTab === 'toolbar' ? 'active' : 'ghost'}
|
||||
onClick={() => handleTabClick('toolbar')}
|
||||
data-tab-button='toolbar'
|
||||
data-tour='tab-toolbar'
|
||||
>
|
||||
Toolbar
|
||||
</Button>
|
||||
@@ -717,6 +719,7 @@ export const Panel = memo(function Panel() {
|
||||
variant={_hasHydrated && activeTab === 'editor' ? 'active' : 'ghost'}
|
||||
onClick={() => handleTabClick('editor')}
|
||||
data-tab-button='editor'
|
||||
data-tour='tab-editor'
|
||||
>
|
||||
Editor
|
||||
</Button>
|
||||
|
||||
@@ -215,6 +215,7 @@ const SidebarNavItem = memo(function SidebarNavItem({
|
||||
<Link
|
||||
href={item.href}
|
||||
data-item-id={item.id}
|
||||
data-tour={`nav-${item.id}`}
|
||||
className={`${baseClasses} ${activeClasses}`}
|
||||
onClick={
|
||||
item.onClick
|
||||
@@ -233,6 +234,7 @@ const SidebarNavItem = memo(function SidebarNavItem({
|
||||
<button
|
||||
type='button'
|
||||
data-item-id={item.id}
|
||||
data-tour={`nav-${item.id}`}
|
||||
className={`${baseClasses} ${activeClasses}`}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
@@ -1139,7 +1141,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
)}
|
||||
>
|
||||
{/* Tasks */}
|
||||
<div className='tasks-section flex flex-shrink-0 flex-col'>
|
||||
<div className='tasks-section flex flex-shrink-0 flex-col' data-tour='nav-tasks'>
|
||||
<div className='flex h-[18px] flex-shrink-0 items-center justify-between px-[16px]'>
|
||||
<div className='font-base text-[var(--text-icon)] text-small'>All tasks</div>
|
||||
{!isCollapsed && (
|
||||
@@ -1253,7 +1255,10 @@ export const Sidebar = memo(function Sidebar() {
|
||||
</div>
|
||||
|
||||
{/* Workflows */}
|
||||
<div className='workflows-section relative mt-[14px] flex flex-col'>
|
||||
<div
|
||||
className='workflows-section relative mt-[14px] flex flex-col'
|
||||
data-tour='nav-workflows'
|
||||
>
|
||||
<div className='flex h-[18px] flex-shrink-0 items-center justify-between px-[16px]'>
|
||||
<div className='font-base text-[var(--text-icon)] text-small'>Workflows</div>
|
||||
{!isCollapsed && (
|
||||
|
||||
@@ -98,6 +98,8 @@ interface PreviewProps {
|
||||
width?: number
|
||||
/** Height of the preview in pixels */
|
||||
height?: number
|
||||
/** Whether video should loop */
|
||||
loop?: boolean
|
||||
/** Optional additional class names */
|
||||
className?: string
|
||||
}
|
||||
@@ -115,20 +117,22 @@ const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg', '.mov'] as const
|
||||
* </Tooltip.Content>
|
||||
* ```
|
||||
*/
|
||||
const Preview = ({ src, alt = '', width = 240, height, className }: PreviewProps) => {
|
||||
const Preview = ({ src, alt = '', width = 240, height, loop = true, className }: PreviewProps) => {
|
||||
const pathname = src.toLowerCase().split('?')[0].split('#')[0]
|
||||
const isVideo = VIDEO_EXTENSIONS.some((ext) => pathname.endsWith(ext))
|
||||
|
||||
return (
|
||||
<div className={cn('mt-[4px] overflow-hidden rounded-[3px]', className)}>
|
||||
<div
|
||||
className={cn('-mx-[8px] -mb-[3.5px] mt-[4px] overflow-hidden rounded-b-[4px]', className)}
|
||||
>
|
||||
{isVideo ? (
|
||||
<video
|
||||
src={src}
|
||||
width={width}
|
||||
height={height}
|
||||
className='block max-w-full'
|
||||
className='block w-full'
|
||||
autoPlay
|
||||
loop
|
||||
loop={loop}
|
||||
muted
|
||||
playsInline
|
||||
preload='none'
|
||||
@@ -140,7 +144,7 @@ const Preview = ({ src, alt = '', width = 240, height, className }: PreviewProps
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
className='block max-w-full'
|
||||
className='block w-full'
|
||||
loading='lazy'
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type * as React from 'react'
|
||||
import { type RefObject, useRef } from 'react'
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||
import { X } from 'lucide-react'
|
||||
import { createPortal } from 'react-dom'
|
||||
@@ -67,8 +68,13 @@ function TourCard({
|
||||
{step} / {totalSteps}
|
||||
</span>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className={cn(isFirst && 'pointer-events-none opacity-0')}>
|
||||
<Button variant='default' size='sm' onClick={onBack}>
|
||||
<div className={cn(isFirst && 'invisible')}>
|
||||
<Button
|
||||
variant='default'
|
||||
size='sm'
|
||||
onClick={onBack}
|
||||
tabIndex={isFirst ? -1 : undefined}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
@@ -141,6 +147,9 @@ function TourTooltip({
|
||||
onClose,
|
||||
className,
|
||||
}: TourTooltipProps) {
|
||||
const virtualRef = useRef<HTMLElement | null>(null)
|
||||
virtualRef.current = targetEl
|
||||
|
||||
if (typeof document === 'undefined') return null
|
||||
if (!isVisible) return null
|
||||
|
||||
@@ -186,7 +195,7 @@ function TourTooltip({
|
||||
|
||||
return createPortal(
|
||||
<PopoverPrimitive.Root open>
|
||||
<PopoverPrimitive.Anchor virtualRef={{ current: targetEl }} />
|
||||
<PopoverPrimitive.Anchor virtualRef={virtualRef as RefObject<HTMLElement>} />
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
side={PLACEMENT_TO_SIDE[placement] || 'bottom'}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user