diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts new file mode 100644 index 0000000000..38779f28cb --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts @@ -0,0 +1,2 @@ +export { NavTour, START_NAV_TOUR_EVENT } from './product-tour' +export { START_WORKFLOW_TOUR_EVENT, WorkflowTour } from './workflow-tour' diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts new file mode 100644 index 0000000000..632d94357d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts @@ -0,0 +1,76 @@ +import type { Step } from 'react-joyride' + +export const navTourSteps: Step[] = [ + { + target: '[data-item-id="home"]', + title: 'Home', + content: + 'Your starting point. Describe what you want to build in plain language or pick a template to get started.', + placement: 'right', + disableBeacon: true, + spotlightPadding: 0, + }, + { + target: '[data-item-id="search"]', + title: 'Search', + content: 'Quickly find workflows, blocks, and tools. Use Cmd+K to open it from anywhere.', + placement: 'right', + disableBeacon: true, + spotlightPadding: 0, + }, + { + target: '[data-item-id="tables"]', + title: 'Tables', + content: + 'Store and query structured data. Your workflows can read and write to tables directly.', + placement: 'right', + disableBeacon: true, + }, + { + target: '[data-item-id="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"]', + 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.', + placement: 'right', + disableBeacon: true, + }, + { + target: '[data-item-id="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.', + placement: 'right', + disableBeacon: true, + }, + { + target: '[data-item-id="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.', + placement: 'right', + disableBeacon: true, + }, + { + target: '.tasks-section', + 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.', + placement: 'right', + disableBeacon: true, + }, + { + target: '.workflows-section', + 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.', + placement: 'right', + disableBeacon: true, + }, +] diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx new file mode 100644 index 0000000000..1c49837afa --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useMemo } from 'react' +import dynamic from 'next/dynamic' +import { usePathname } from 'next/navigation' +import { navTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps' +import type { TourState } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' +import { + getSharedJoyrideProps, + TourStateContext, + TourTooltipAdapter, +} from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' +import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' + +const Joyride = dynamic(() => import('react-joyride'), { + ssr: false, +}) + +const NAV_TOUR_STORAGE_KEY = 'sim-nav-tour-completed-v1' +export const START_NAV_TOUR_EVENT = 'start-nav-tour' + +export function NavTour() { + const pathname = usePathname() + const isWorkflowPage = /\/w\/[^/]+/.test(pathname) + + const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({ + steps: navTourSteps, + storageKey: NAV_TOUR_STORAGE_KEY, + autoStartDelay: 1200, + resettable: true, + triggerEvent: START_NAV_TOUR_EVENT, + tourName: 'Navigation tour', + disabled: isWorkflowPage, + }) + + const tourState = useMemo( + () => ({ + isTooltipVisible, + isEntrance, + totalSteps: navTourSteps.length, + }), + [isTooltipVisible, isEntrance] + ) + + return ( + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx new file mode 100644 index 0000000000..b0d7436ac4 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx @@ -0,0 +1,157 @@ +'use client' + +import { createContext, useCallback, useContext, useEffect, useState } from 'react' +import type { TooltipRenderProps } from 'react-joyride' +import { TourTooltip } from '@/components/emcn' + +/** Shared state passed from the tour component to the tooltip adapter via context */ +export interface TourState { + isTooltipVisible: boolean + isEntrance: boolean + totalSteps: number +} + +export const TourStateContext = createContext({ + isTooltipVisible: true, + isEntrance: true, + totalSteps: 0, +}) + +/** + * Maps Joyride placement strings to TourTooltip placement values. + */ +function mapPlacement(placement?: string): 'top' | 'right' | 'bottom' | 'left' | 'center' { + switch (placement) { + case 'top': + case 'top-start': + case 'top-end': + return 'top' + case 'right': + case 'right-start': + case 'right-end': + return 'right' + case 'bottom': + case 'bottom-start': + case 'bottom-end': + return 'bottom' + case 'left': + case 'left-start': + case 'left-end': + return 'left' + case 'center': + return 'center' + default: + return 'bottom' + } +} + +/** + * Adapter that bridges Joyride's tooltip render props to the EMCN TourTooltip component. + * Reads transition state from TourStateContext to coordinate fade animations. + */ +export function TourTooltipAdapter({ + step, + index, + isLastStep, + tooltipProps, + primaryProps, + backProps, + closeProps, +}: TooltipRenderProps) { + const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) + const [targetEl, setTargetEl] = useState(null) + + useEffect(() => { + const { target } = step + if (typeof target === 'string') { + setTargetEl(document.querySelector(target)) + } else if (target instanceof HTMLElement) { + setTargetEl(target) + } else { + setTargetEl(null) + } + }, [step]) + + const refCallback = useCallback( + (node: HTMLDivElement | null) => { + if (tooltipProps.ref) { + ;(tooltipProps.ref as React.RefCallback)(node) + } + }, + [tooltipProps.ref] + ) + + const placement = mapPlacement(step.placement) + + return ( + <> +
+ void} + onBack={backProps.onClick as () => void} + onClose={closeProps.onClick as () => void} + /> + + ) +} + +const SPOTLIGHT_TRANSITION = + 'top 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), left 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), width 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), height 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)' + +/** + * Returns the shared Joyride floaterProps and styles config used by both tours. + * Only `spotlightPadding` and spotlight `borderRadius` differ between tours. + */ +export function getSharedJoyrideProps(overrides: { spotlightBorderRadius: number }) { + return { + floaterProps: { + disableAnimation: true, + hideArrow: true, + styles: { + floater: { + filter: 'none', + opacity: 0, + pointerEvents: 'none' as React.CSSProperties['pointerEvents'], + width: 0, + height: 0, + }, + }, + }, + styles: { + options: { + zIndex: 10000, + }, + spotlight: { + backgroundColor: 'transparent', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: overrides.spotlightBorderRadius, + boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.55)', + position: 'fixed' as React.CSSProperties['position'], + transition: SPOTLIGHT_TRANSITION, + }, + overlay: { + backgroundColor: 'transparent', + mixBlendMode: 'unset' as React.CSSProperties['mixBlendMode'], + position: 'fixed' as React.CSSProperties['position'], + height: '100%', + overflow: 'visible', + pointerEvents: 'none' as React.CSSProperties['pointerEvents'], + }, + }, + } as const +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts new file mode 100644 index 0000000000..03358b0965 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts @@ -0,0 +1,260 @@ +'use client' + +import { useCallback, useEffect, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' +import { ACTIONS, type CallBackProps, EVENTS, STATUS, type Step } from 'react-joyride' + +const logger = createLogger('useTour') + +/** Transition delay before updating step index (ms) */ +const FADE_OUT_MS = 80 + +interface UseTourOptions { + /** Tour step definitions */ + steps: Step[] + /** localStorage key for completion persistence */ + storageKey: string + /** Delay before auto-starting the tour (ms) */ + autoStartDelay?: number + /** Whether this tour can be reset/retriggered */ + resettable?: boolean + /** Custom event name to listen for manual triggers */ + triggerEvent?: string + /** Identifier for logging */ + tourName?: string + /** When true, suppresses auto-start (e.g. to avoid overlapping with another active tour) */ + disabled?: boolean +} + +interface UseTourReturn { + /** Whether the tour is currently running */ + run: boolean + /** Current step index */ + stepIndex: number + /** Key to force Joyride remount on retrigger */ + tourKey: number + /** Whether the tooltip is visible (false during step transitions) */ + isTooltipVisible: boolean + /** Whether this is the initial entrance animation */ + isEntrance: boolean + /** Joyride callback handler */ + handleCallback: (data: CallBackProps) => void +} + +function isTourCompleted(storageKey: string): boolean { + try { + return localStorage.getItem(storageKey) === 'true' + } catch { + return false + } +} + +function markTourCompleted(storageKey: string): void { + try { + localStorage.setItem(storageKey, 'true') + } catch { + logger.warn('Failed to persist tour completion', { storageKey }) + } +} + +function clearTourCompletion(storageKey: string): void { + try { + localStorage.removeItem(storageKey) + } catch { + logger.warn('Failed to clear tour completion', { storageKey }) + } +} + +/** + * Shared hook for managing product tour state with smooth transitions. + * + * Handles auto-start on first visit, localStorage persistence, + * manual triggering via custom events, and coordinated fade + * transitions between steps to prevent layout shift. + */ +export function useTour({ + steps, + storageKey, + autoStartDelay = 1200, + resettable = false, + triggerEvent, + tourName = 'tour', + disabled = false, +}: UseTourOptions): UseTourReturn { + const [run, setRun] = useState(false) + const [stepIndex, setStepIndex] = useState(0) + const [tourKey, setTourKey] = useState(0) + const [isTooltipVisible, setIsTooltipVisible] = useState(true) + const [isEntrance, setIsEntrance] = useState(true) + + const hasAutoStarted = useRef(false) + const retriggerTimerRef = useRef | null>(null) + const transitionTimerRef = useRef | null>(null) + + const stopTour = useCallback(() => { + setRun(false) + setIsTooltipVisible(true) + setIsEntrance(true) + markTourCompleted(storageKey) + }, [storageKey]) + + /** Transition to a new step with a coordinated fade-out/fade-in */ + const transitionToStep = useCallback( + (newIndex: number) => { + if (newIndex < 0 || newIndex >= steps.length) { + stopTour() + return + } + + /** Hide tooltip during transition */ + setIsTooltipVisible(false) + + if (transitionTimerRef.current) { + clearTimeout(transitionTimerRef.current) + } + + 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) + }) + }) + }, FADE_OUT_MS) + }, + [steps.length, stopTour] + ) + + /** Stop the tour when disabled becomes true (e.g. navigating away from the relevant page) */ + useEffect(() => { + if (disabled && run) { + setRun(false) + setIsTooltipVisible(true) + setIsEntrance(true) + logger.info(`${tourName} paused — disabled became true`) + } + }, [disabled, run, tourName]) + + /** Auto-start on first visit */ + useEffect(() => { + if (disabled || hasAutoStarted.current) return + + const timer = setTimeout(() => { + hasAutoStarted.current = true + if (!isTourCompleted(storageKey)) { + setStepIndex(0) + setIsEntrance(true) + setIsTooltipVisible(false) + setRun(true) + logger.info(`Auto-starting ${tourName}`) + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setIsTooltipVisible(true) + }) + }) + } + }, autoStartDelay) + + return () => clearTimeout(timer) + }, [storageKey, autoStartDelay, tourName, disabled]) + + /** Listen for manual trigger events */ + useEffect(() => { + if (!triggerEvent || !resettable) return + + const handleTrigger = () => { + setRun(false) + clearTourCompletion(storageKey) + setTourKey((k) => k + 1) + + if (retriggerTimerRef.current) { + 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) + setIsEntrance(true) + setIsTooltipVisible(false) + setRun(true) + logger.info(`${tourName} triggered via event`) + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setIsTooltipVisible(true) + }) + }) + }, 50) + } + + window.addEventListener(triggerEvent, handleTrigger) + return () => { + window.removeEventListener(triggerEvent, handleTrigger) + if (retriggerTimerRef.current) { + clearTimeout(retriggerTimerRef.current) + } + } + }, [triggerEvent, resettable, storageKey, tourName]) + + useEffect(() => { + return () => { + if (transitionTimerRef.current) { + clearTimeout(transitionTimerRef.current) + } + } + }, []) + + const handleCallback = useCallback( + (data: CallBackProps) => { + const { action, index, status, type } = data + + if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { + stopTour() + logger.info(`${tourName} ended`, { status }) + return + } + + if (type === EVENTS.STEP_AFTER || type === EVENTS.TARGET_NOT_FOUND) { + if (action === ACTIONS.CLOSE) { + stopTour() + logger.info(`${tourName} closed by user`) + return + } + + const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1) + + if (type === EVENTS.TARGET_NOT_FOUND) { + logger.info(`${tourName} step target not found, skipping`, { + stepIndex: index, + target: steps[index]?.target, + }) + } + + transitionToStep(nextIndex) + } + }, + [stopTour, transitionToStep, steps, tourName] + ) + + return { + run, + stepIndex, + tourKey, + isTooltipVisible, + isEntrance, + handleCallback, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts new file mode 100644 index 0000000000..4a42555869 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts @@ -0,0 +1,56 @@ +import type { Step } from 'react-joyride' + +export const workflowTourSteps: Step[] = [ + { + target: '[data-tour="canvas"]', + title: 'The Canvas', + content: + 'This is where you build visually. Drag blocks onto the canvas and connect them to create AI workflows.', + placement: 'center', + disableBeacon: true, + }, + { + target: '[data-tab-button="copilot"]', + title: 'AI Copilot', + content: + 'Build and debug workflows using natural language. Describe what you want and Copilot creates the blocks for you.', + placement: 'bottom', + disableBeacon: true, + spotlightPadding: 0, + }, + { + target: '[data-tab-button="toolbar"]', + title: 'Block Library', + content: + 'Browse all available blocks and triggers. Drag them onto the canvas to build your workflow step by step.', + placement: 'bottom', + disableBeacon: true, + spotlightPadding: 0, + }, + { + target: '[data-tab-button="editor"]', + title: 'Block Editor', + content: + 'Click any block on the canvas to configure it here. Set inputs, credentials, and fine-tune behavior.', + placement: 'bottom', + disableBeacon: true, + spotlightPadding: 0, + }, + { + target: '[data-tour="deploy-run"]', + title: 'Deploy & Run', + content: + 'Deploy your workflow as an API, webhook, schedule, or chat widget. Then hit Run to test it out.', + placement: 'bottom', + disableBeacon: true, + }, + { + target: '[data-tour="workflow-controls"]', + title: 'Canvas Controls', + content: + 'Switch between pointer and hand mode, undo or redo changes, and fit the canvas to your view.', + placement: 'top', + spotlightPadding: 0, + disableBeacon: true, + }, +] diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx new file mode 100644 index 0000000000..13bcf7468c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx @@ -0,0 +1,62 @@ +'use client' + +import { useMemo } from 'react' +import dynamic from 'next/dynamic' +import type { TourState } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' +import { + getSharedJoyrideProps, + TourStateContext, + TourTooltipAdapter, +} from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' +import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' +import { workflowTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps' + +const Joyride = dynamic(() => import('react-joyride'), { + ssr: false, +}) + +const WORKFLOW_TOUR_STORAGE_KEY = 'sim-workflow-tour-completed-v1' +export const START_WORKFLOW_TOUR_EVENT = 'start-workflow-tour' + +/** + * Workflow tour that covers the canvas, blocks, copilot, and deployment. + * Runs on first workflow visit and can be retriggered via "Take a tour". + */ +export function WorkflowTour() { + const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({ + steps: workflowTourSteps, + storageKey: WORKFLOW_TOUR_STORAGE_KEY, + autoStartDelay: 800, + resettable: true, + triggerEvent: START_WORKFLOW_TOUR_EVENT, + tourName: 'Workflow tour', + }) + + const tourState = useMemo( + () => ({ + isTooltipVisible, + isEntrance, + totalSteps: workflowTourSteps.length, + }), + [isTooltipVisible, isEntrance] + ) + + return ( + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index f681db98c6..1f5c5b785a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -322,11 +322,14 @@ export function Home({ chatId }: HomeProps = {}) { return (
-

+

What should we get done {session?.user?.name ? `, ${session.user.name.split(' ')[0]}` : ''}?

-
+
diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index 45a92298f3..dba9198ba5 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -1,4 +1,5 @@ import { ToastProvider } from '@/components/emcn' +import { NavTour } from '@/app/workspace/[workspaceId]/components/product-tour' import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader' import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader' @@ -21,6 +22,7 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod {children}
+
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index 9ec049a209..d4ebc9f75e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -1,5 +1,4 @@ import { - BookOpen, Card, Connections, HexSimple, @@ -38,7 +37,6 @@ export type SettingsSection = | 'skills' | 'workflow-mcp-servers' | 'inbox' - | 'docs' | 'admin' | 'recently-deleted' @@ -156,14 +154,6 @@ export const allNavigationItems: NavigationItem[] = [ requiresEnterprise: true, selfHostedOverride: isSSOEnabled, }, - { - id: 'docs', - label: 'Docs', - icon: BookOpen, - section: 'system', - requiresHosted: true, - externalUrl: 'https://docs.sim.ai', - }, { id: 'admin', label: 'Admin', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx index f115a655f3..6fe1eb3b9a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx @@ -179,6 +179,7 @@ export function CommandList() { )} >
- @@ -668,10 +668,11 @@ export const Panel = memo(function Panel() {
{/* Deploy and Run */} -
+
+ + + {showCollapsedContent && ( + +

Help

+
+ )} + + + + window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer') + } + > + + Docs + + setIsHelpModalOpen(true)}> + + Report an issue + + + + Take a tour + + + + {footerItems.map((item) => ( - {children} + {showArrow ? ( +
+ {children} +
+ ) : ( + children + )} + {showArrow && ( + + + + + + + )} ) diff --git a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx new file mode 100644 index 0000000000..d4d62dade6 --- /dev/null +++ b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx @@ -0,0 +1,224 @@ +'use client' + +import type * as React from 'react' +import * as PopoverPrimitive from '@radix-ui/react-popover' +import { X } from 'lucide-react' +import { createPortal } from 'react-dom' +import { Button } from '@/components/emcn/components/button/button' +import { cn } from '@/lib/core/utils/cn' + +type TourTooltipPlacement = 'top' | 'right' | 'bottom' | 'left' | 'center' + +interface TourCardProps { + /** Title displayed in the card header */ + title: string + /** Description text in the card body */ + description: React.ReactNode + /** Current step number (1-based) */ + step: number + /** Total number of steps in the tour */ + totalSteps: number + /** Whether this is the first step (hides Back button) */ + isFirst?: boolean + /** Whether this is the last step (changes Next to Done) */ + isLast?: boolean + /** Called when the user clicks Next or Done */ + onNext?: () => void + /** Called when the user clicks Back */ + onBack?: () => void + /** Called when the user dismisses the tour */ + onClose?: () => void +} + +function TourCard({ + title, + description, + step, + totalSteps, + isFirst, + isLast, + onNext, + onBack, + onClose, +}: TourCardProps) { + return ( + <> +
+

+ {title} +

+ +
+ +
+

{description}

+
+ +
+ + {step} / {totalSteps} + +
+
+ +
+ +
+
+ + ) +} + +interface TourTooltipProps extends TourCardProps { + /** Placement relative to the target element */ + placement?: TourTooltipPlacement + /** Target DOM element to anchor the tooltip to */ + targetEl: HTMLElement | null + /** Controls tooltip visibility for smooth transitions */ + isVisible?: boolean + /** Whether this is the initial entrance (plays full entrance animation) */ + isEntrance?: boolean + /** Additional class names for the tooltip card */ + className?: string +} + +const PLACEMENT_TO_SIDE: Record< + Exclude, + 'top' | 'right' | 'bottom' | 'left' +> = { + top: 'top', + right: 'right', + bottom: 'bottom', + left: 'left', +} + +/** + * A positioned tooltip component for guided product tours. + * + * Anchors to a target DOM element using Radix Popover primitives for + * collision-aware positioning. Supports centered placement for overlay steps. + * The card surface matches the emcn Modal / DropdownMenu conventions. + * + * @example + * ```tsx + * + * ``` + */ +function TourTooltip({ + title, + description, + step, + totalSteps, + placement = 'bottom', + targetEl, + isFirst = false, + isLast = false, + isVisible = true, + isEntrance = false, + onNext, + onBack, + onClose, + className, +}: TourTooltipProps) { + if (typeof document === 'undefined') return null + if (!isVisible) return null + + const isCentered = placement === 'center' + + const cardClasses = cn( + 'w-[260px] overflow-hidden rounded-[8px] bg-[var(--bg)]', + isEntrance && 'animate-tour-tooltip-in motion-reduce:animate-none', + className + ) + + const cardContent = ( + + ) + + if (isCentered) { + return createPortal( +
+
+ {cardContent} +
+
, + document.body + ) + } + + if (!targetEl) return null + + return createPortal( + + + + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > +
{cardContent}
+ + + + + + +
+
+
, + document.body + ) +} + +export { TourCard, TourTooltip } +export type { TourCardProps, TourTooltipPlacement, TourTooltipProps } diff --git a/apps/sim/package.json b/apps/sim/package.json index e7abd4a8fd..c588585b3d 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -154,6 +154,7 @@ "react": "19.2.4", "react-dom": "19.2.4", "react-hook-form": "^7.54.2", + "react-joyride": "2.9.3", "react-markdown": "^10.1.0", "react-simple-code-editor": "^0.14.1", "react-window": "2.2.3", @@ -226,6 +227,10 @@ "next": "16.1.6", "@next/env": "16.1.6", "drizzle-orm": "^0.44.5", - "postgres": "^3.4.5" + "postgres": "^3.4.5", + "react-floater": { + "react": "$react", + "react-dom": "$react-dom" + } } } diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts index f12f246837..fd6a8bf29d 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -180,6 +180,10 @@ export default { from: { opacity: '0', transform: 'translateY(20px)' }, to: { opacity: '1', transform: 'translateY(0)' }, }, + 'tour-tooltip-in': { + from: { opacity: '0', transform: 'scale(0.96) translateY(4px)' }, + to: { opacity: '1', transform: 'scale(1) translateY(0)' }, + }, }, animation: { 'caret-blink': 'caret-blink 1.25s ease-out infinite', @@ -193,6 +197,7 @@ export default { 'thinking-block': 'thinking-block 1.6s ease-in-out infinite', 'slide-in-right': 'slide-in-right 350ms ease-out forwards', 'slide-in-bottom': 'slide-in-bottom 400ms cubic-bezier(0.16, 1, 0.3, 1)', + 'tour-tooltip-in': 'tour-tooltip-in 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, }, }, diff --git a/bun.lock b/bun.lock index c0d7bcc9df..ed31b1d954 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -178,6 +179,7 @@ "react": "19.2.4", "react-dom": "19.2.4", "react-hook-form": "^7.54.2", + "react-joyride": "2.9.3", "react-markdown": "^10.1.0", "react-simple-code-editor": "^0.14.1", "react-window": "2.2.3", @@ -714,6 +716,8 @@ "@fumari/stf": ["@fumari/stf@1.0.2", "", { "peerDependencies": { "@types/react": "*", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@types/react"] }, "sha512-AsrCbI0pfnxRGeXLeaC6tpMXi7NLHU+l9dfjenCgyCaLFGW16sAccznAgcsAO2sRIH4qXa/D8TLrbHB+hhytfg=="], + "@gilbarbara/deep-equal": ["@gilbarbara/deep-equal@0.3.1", "", {}, "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw=="], + "@google-cloud/precise-date": ["@google-cloud/precise-date@4.0.0", "", {}, "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA=="], "@google/genai": ["@google/genai@1.34.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.24.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw=="], @@ -2034,6 +2038,8 @@ "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "deep-diff": ["deep-diff@1.0.2", "", {}, "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], @@ -2502,6 +2508,8 @@ "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-lite": ["is-lite@1.2.1", "", {}, "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], @@ -2672,6 +2680,8 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="], "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], @@ -3084,6 +3094,8 @@ "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "popper.js": ["popper.js@1.16.1", "", {}, "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], @@ -3130,6 +3142,8 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], @@ -3176,8 +3190,16 @@ "react-email": ["react-email@4.3.2", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/traverse": "^7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", "normalize-path": "^3.0.0", "nypm": "0.6.0", "ora": "^8.0.0", "prompts": "2.4.2", "socket.io": "^4.8.1", "tsconfig-paths": "4.2.0" }, "bin": { "email": "dist/index.js" } }, "sha512-WaZcnv9OAIRULY236zDRdk+8r511ooJGH5UOb7FnVsV33hGPI+l5aIZ6drVjXi4QrlLTmLm8PsYvmXRSv31MPA=="], + "react-floater": ["react-floater@0.7.9", "", { "dependencies": { "deepmerge": "^4.3.1", "is-lite": "^0.8.2", "popper.js": "^1.16.0", "prop-types": "^15.8.1", "tree-changes": "^0.9.1" }, "peerDependencies": { "react": "15 - 18", "react-dom": "15 - 18" } }, "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg=="], + "react-hook-form": ["react-hook-form@7.72.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw=="], + "react-innertext": ["react-innertext@1.1.5", "", { "peerDependencies": { "@types/react": ">=0.0.0 <=99", "react": ">=0.0.0 <=99" } }, "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-joyride": ["react-joyride@2.9.3", "", { "dependencies": { "@gilbarbara/deep-equal": "^0.3.1", "deep-diff": "^1.0.2", "deepmerge": "^4.3.1", "is-lite": "^1.2.1", "react-floater": "^0.7.9", "react-innertext": "^1.1.5", "react-is": "^16.13.1", "scroll": "^3.0.1", "scrollparent": "^2.1.0", "tree-changes": "^0.11.2", "type-fest": "^4.27.0" }, "peerDependencies": { "react": "15 - 18", "react-dom": "15 - 18" } }, "sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], "react-medium-image-zoom": ["react-medium-image-zoom@5.4.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g=="], @@ -3310,8 +3332,12 @@ "scmp": ["scmp@2.1.0", "", {}, "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="], + "scroll": ["scroll@3.0.1", "", {}, "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg=="], + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], + "scrollparent": ["scrollparent@2.1.0", "", {}, "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA=="], + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], @@ -3544,6 +3570,8 @@ "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "tree-changes": ["tree-changes@0.11.3", "", { "dependencies": { "@gilbarbara/deep-equal": "^0.3.1", "is-lite": "^1.2.1" } }, "sha512-r14mvDZ6tqz8PRQmlFKjhUVngu4VZ9d92ON3tp0EGpFBE6PAHOq8Bx8m8ahbNoGE3uI/npjYcJiqVydyOiYXag=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -3574,7 +3602,7 @@ "twilio": ["twilio@5.9.0", "", { "dependencies": { "axios": "^1.11.0", "dayjs": "^1.11.9", "https-proxy-agent": "^5.0.0", "jsonwebtoken": "^9.0.2", "qs": "^6.9.4", "scmp": "^2.1.0", "xmlbuilder": "^13.0.2" } }, "sha512-Ij+xT9MZZSjP64lsy+x6vYsCCb5m2Db9KffkMXBrN3zWbG3rbkXxl+MZVVzrvpwEdSbQD0vMuin+TTlQ6kR6Xg=="], - "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], @@ -4114,6 +4142,8 @@ "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -4256,6 +4286,8 @@ "log-update/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "mammoth/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -4338,6 +4370,10 @@ "react-email/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "react-floater/is-lite": ["is-lite@0.8.2", "", {}, "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw=="], + + "react-floater/tree-changes": ["tree-changes@0.9.3", "", { "dependencies": { "@gilbarbara/deep-equal": "^0.1.1", "is-lite": "^0.8.2" } }, "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ=="], + "react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="], "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -4838,6 +4874,8 @@ "react-email/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "react-floater/tree-changes/@gilbarbara/deep-equal": ["@gilbarbara/deep-equal@0.1.2", "", {}, "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA=="], + "readable-web-to-node-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],