From 68751f2eeebda5b45f1e76f8b3f3bdd728799431 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Wed, 18 Mar 2026 23:23:20 +0530 Subject: [PATCH 01/19] feat: add product tour --- apps/sim/app/_styles/globals.css | 9 + .../components/product-tour/index.ts | 1 + .../components/product-tour/product-tour.tsx | 164 ++++++++++++++++++ .../components/product-tour/tour-steps.ts | 107 ++++++++++++ .../components/product-tour/tour-tooltip.tsx | 78 +++++++++ .../app/workspace/[workspaceId]/home/home.tsx | 8 +- .../app/workspace/[workspaceId]/layout.tsx | 2 + .../components/command-list/command-list.tsx | 1 + .../w/[workflowId]/components/panel/panel.tsx | 5 +- .../workflow-controls/workflow-controls.tsx | 1 + .../[workspaceId]/w/[workflowId]/workflow.tsx | 6 +- .../w/components/sidebar/sidebar.tsx | 48 ++++- apps/sim/package.json | 1 + bun.lock | 40 ++++- 14 files changed, 458 insertions(+), 13 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index 6512d7212f1..47deb43b589 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -920,3 +920,12 @@ input[type="search"]::-ms-clear { .react-flow__node[data-parent-node-id] .react-flow__handle { z-index: 30; } + +.__floater__arrow > span > svg { + fill: #30d158 !important; +} + +.__floater__arrow > span > svg > path { + fill: #30d158 !important; +} + 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 00000000000..7713bf725fa --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts @@ -0,0 +1 @@ +export { ProductTour, resetTourCompletion, START_TOUR_EVENT } from './product-tour' 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 00000000000..2109695be61 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -0,0 +1,164 @@ +'use client' + +import { useCallback, useEffect, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' +import dynamic from 'next/dynamic' +import { ACTIONS, type CallBackProps, EVENTS, STATUS } from 'react-joyride' +import { tourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/tour-steps' +import { TourTooltip } from '@/app/workspace/[workspaceId]/components/product-tour/tour-tooltip' + +const logger = createLogger('ProductTour') + +const Joyride = dynamic(() => import('react-joyride'), { + ssr: false, +}) + +const TOUR_STORAGE_KEY = 'sim-tour-completed-v1' +export const START_TOUR_EVENT = 'start-product-tour' + +function isTourCompleted(): boolean { + try { + return localStorage.getItem(TOUR_STORAGE_KEY) === 'true' + } catch { + return false + } +} + +function markTourCompleted(): void { + try { + localStorage.setItem(TOUR_STORAGE_KEY, 'true') + } catch { + logger.warn('Failed to persist tour completion to localStorage') + } +} + +export function resetTourCompletion(): void { + try { + localStorage.removeItem(TOUR_STORAGE_KEY) + } catch { + logger.warn('Failed to reset tour completion in localStorage') + } +} + +export function ProductTour() { + const [run, setRun] = useState(false) + const [stepIndex, setStepIndex] = useState(0) + const [tourKey, setTourKey] = useState(0) + + const hasAutoStarted = useRef(false) + + useEffect(() => { + if (hasAutoStarted.current) return + hasAutoStarted.current = true + + const timer = setTimeout(() => { + if (!isTourCompleted()) { + setStepIndex(0) + setRun(true) + logger.info('Auto-starting product tour for first-time user') + } + }, 1200) + + return () => clearTimeout(timer) + }, []) + + useEffect(() => { + const handleStartTour = () => { + setRun(false) + resetTourCompletion() + + setTourKey((k) => k + 1) + setTimeout(() => { + setStepIndex(0) + setRun(true) + logger.info('Product tour triggered via custom event') + }, 50) + } + + window.addEventListener(START_TOUR_EVENT, handleStartTour) + return () => window.removeEventListener(START_TOUR_EVENT, handleStartTour) + }, []) + + const stopTour = useCallback(() => { + setRun(false) + markTourCompleted() + }, []) + + const handleCallback = useCallback( + (data: CallBackProps) => { + const { action, index, status, type } = data + + if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { + stopTour() + logger.info('Product tour ended', { status }) + return + } + + if (type === EVENTS.STEP_AFTER || type === EVENTS.TARGET_NOT_FOUND) { + if (action === ACTIONS.CLOSE) { + stopTour() + logger.info('Product tour closed by user') + return + } + + const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1) + + if (type === EVENTS.TARGET_NOT_FOUND) { + logger.info('Tour step target not found, skipping', { + stepIndex: index, + target: tourSteps[index]?.target, + }) + } + + if (nextIndex >= tourSteps.length || nextIndex < 0) { + stopTour() + return + } + + setStepIndex(nextIndex) + } + }, + [stopTour] + ) + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts new file mode 100644 index 00000000000..91a683e841a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts @@ -0,0 +1,107 @@ +import type { Step } from 'react-joyride' + +export const tourSteps: Step[] = [ + { + target: '[data-tour="home-greeting"]', + title: 'Welcome to Sim', + content: + 'This is your home base. From here you can describe what you want to build in plain language, or pick a template to get started.', + placement: 'bottom', + disableBeacon: true, + }, + { + target: '[data-tour="home-chat-input"]', + title: 'Describe your workflow', + content: + 'Type what you want to automate — like "monitor my inbox and summarize new emails." Sim will build an AI workflow for you.', + placement: 'bottom', + disableBeacon: true, + }, + { + target: '[data-tour="home-templates"]', + title: 'Start from a template', + content: + 'Or pick one of these pre-built templates to ship your agent in minutes. Click any card to get started.', + placement: 'top', + disableBeacon: true, + }, + { + target: '.sidebar-container', + title: 'Sidebar navigation', + content: + 'Access everything from here — workflows, tables, files, knowledge base, and logs. This stays with you across all pages.', + placement: 'right', + disableBeacon: true, + }, + { + target: '[data-item-id="search"]', + title: 'Search anything', + content: 'Use search (or Cmd+K) to quickly find workflows, blocks, tools, and more.', + placement: 'right', + disableBeacon: true, + }, + { + target: '.workflows-section', + title: 'Your workflows', + content: + 'All your workflows live here. Create new ones with the + button, organize with folders, and switch between them.', + placement: 'right', + disableBeacon: true, + }, + { + target: '[data-tour="canvas"]', + title: 'The workflow canvas', + content: + 'This is where you build visually. Drag blocks onto the canvas and connect them together to create AI workflows.', + placement: 'center', + disableBeacon: true, + }, + { + target: '[data-tour="command-list"]', + title: 'Quick actions', + content: + 'Use these keyboard shortcuts to get started fast. Try Cmd+K to search for blocks, or Cmd+Y to browse templates.', + placement: 'right', + disableBeacon: true, + }, + { + target: '[data-tab-button="toolbar"]', + title: 'Block library', + content: + 'The Toolbar is your block library. Drag triggers and blocks onto the canvas to build your workflow step by step.', + placement: 'bottom', + disableBeacon: true, + }, + { + target: '[data-tab-button="copilot"]', + title: 'AI Copilot', + content: + 'Copilot helps you build and debug workflows using natural language. Describe what you want and it creates blocks for you.', + placement: 'bottom', + disableBeacon: true, + }, + { + 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, + }, + { + target: '[data-tour="deploy-run"]', + title: 'Run and deploy', + content: + 'Hit Run to execute your workflow and see results in the terminal below. When ready, Deploy as an API, webhook, schedule, or chat widget.', + placement: 'bottom', + disableBeacon: true, + }, + { + target: '[data-tour="workflow-controls"]', + title: 'Canvas controls', + content: + 'Switch between pointer and hand mode, undo/redo changes, and fit your canvas to view.', + placement: 'top', + disableBeacon: true, + }, +] diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx new file mode 100644 index 00000000000..b14a63dcfd8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx @@ -0,0 +1,78 @@ +'use client' + +import type { TooltipRenderProps } from 'react-joyride' +import { cn } from '@/lib/core/utils/cn' + +export function TourTooltip({ + continuous, + index, + step, + backProps, + closeProps, + primaryProps, + skipProps, + isLastStep, + tooltipProps, +}: TooltipRenderProps) { + return ( +
+
+ {step.title && ( +

+ {step.title as string} +

+ )} +
+
+

{step.content}

+
+
+
+ {!isLastStep && ( + + )} +
+
+ {index > 0 && ( + + )} + {continuous ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index f681db98c6a..1f5c5b785a2 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 45a92298f32..da884b92573 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 { ProductTour } 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]/w/[workflowId]/components/command-list/command-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx index f115a655f3e..83f8e0ae20c 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 @@ -174,6 +174,7 @@ export function CommandList() { return (
- @@ -668,10 +668,11 @@ export const Panel = memo(function Panel() {
{/* Deploy and Run */} -
+
@@ -1390,6 +1389,41 @@ export const Sidebar = memo(function Sidebar() { !hasOverflowBottom && 'border-transparent' )} > + {/* Help dropdown */} + + + + + + + + {showCollapsedContent && ( + +

Help

+
+ )} +
+ + setIsHelpModalOpen(true)}> + + Report an issue + + + + Take a tour + + +
+ {footerItems.map((item) => ( =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=="], From 6b26102afc66da3eeaa8f72668e85f79806a6f53 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Thu, 19 Mar 2026 01:44:32 +0530 Subject: [PATCH 02/19] chore: updated modals --- apps/sim/app/_styles/globals.css | 9 - .../components/product-tour/product-tour.tsx | 25 ++- .../components/product-tour/tour-tooltip.tsx | 203 +++++++++++++++--- .../w/components/sidebar/sidebar.tsx | 3 +- .../emcn/components/popover/popover.tsx | 38 +++- apps/sim/package.json | 6 +- apps/sim/tailwind.config.ts | 5 + 7 files changed, 238 insertions(+), 51 deletions(-) diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index 47deb43b589..6512d7212f1 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -920,12 +920,3 @@ input[type="search"]::-ms-clear { .react-flow__node[data-parent-node-id] .react-flow__handle { z-index: 30; } - -.__floater__arrow > span > svg { - fill: #30d158 !important; -} - -.__floater__arrow > span > svg > path { - fill: #30d158 !important; -} - 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 index 2109695be61..c25d71c16f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -4,8 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import dynamic from 'next/dynamic' import { ACTIONS, type CallBackProps, EVENTS, STATUS } from 'react-joyride' -import { tourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/tour-steps' -import { TourTooltip } from '@/app/workspace/[workspaceId]/components/product-tour/tour-tooltip' +import { tourSteps } from './tour-steps' +import { TourTooltip } from './tour-tooltip' const logger = createLogger('ProductTour') @@ -46,6 +46,7 @@ export function ProductTour() { const [tourKey, setTourKey] = useState(0) const hasAutoStarted = useRef(false) + const retriggerTimerRef = useRef | null>(null) useEffect(() => { if (hasAutoStarted.current) return @@ -68,7 +69,13 @@ export function ProductTour() { resetTourCompletion() setTourKey((k) => k + 1) - setTimeout(() => { + + if (retriggerTimerRef.current) { + clearTimeout(retriggerTimerRef.current) + } + + retriggerTimerRef.current = setTimeout(() => { + retriggerTimerRef.current = null setStepIndex(0) setRun(true) logger.info('Product tour triggered via custom event') @@ -76,7 +83,12 @@ export function ProductTour() { } window.addEventListener(START_TOUR_EVENT, handleStartTour) - return () => window.removeEventListener(START_TOUR_EVENT, handleStartTour) + return () => { + window.removeEventListener(START_TOUR_EVENT, handleStartTour) + if (retriggerTimerRef.current) { + clearTimeout(retriggerTimerRef.current) + } + } }, []) const stopTour = useCallback(() => { @@ -137,9 +149,14 @@ export function ProductTour() { tooltipComponent={TourTooltip} floaterProps={{ disableAnimation: true, + hideArrow: true, styles: { floater: { filter: 'none', + opacity: 0, + pointerEvents: 'none' as React.CSSProperties['pointerEvents'], + width: 0, + height: 0, }, }, }} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx index b14a63dcfd8..e5c52ac9ee7 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx @@ -1,30 +1,67 @@ 'use client' +import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import type { TooltipRenderProps } from 'react-joyride' -import { cn } from '@/lib/core/utils/cn' +import { Button, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn' -export function TourTooltip({ +function mapPlacement(placement?: string): { + side: 'top' | 'right' | 'bottom' | 'left' + align: 'start' | 'center' | 'end' +} { + switch (placement) { + case 'top': + case 'top-start': + return { side: 'top', align: 'center' } + case 'top-end': + return { side: 'top', align: 'end' } + case 'right': + case 'right-start': + return { side: 'right', align: 'center' } + case 'right-end': + return { side: 'right', align: 'end' } + case 'bottom': + case 'bottom-start': + return { side: 'bottom', align: 'center' } + case 'bottom-end': + return { side: 'bottom', align: 'end' } + case 'left': + case 'left-start': + return { side: 'left', align: 'center' } + case 'left-end': + return { side: 'left', align: 'end' } + case 'center': + return { side: 'bottom', align: 'center' } + default: + return { side: 'bottom', align: 'center' } + } +} + +function TourTooltipBody({ + step, continuous, index, - step, + isLastStep, backProps, closeProps, primaryProps, skipProps, - isLastStep, - tooltipProps, -}: TooltipRenderProps) { +}: Pick< + TooltipRenderProps, + | 'step' + | 'continuous' + | 'index' + | 'isLastStep' + | 'backProps' + | 'closeProps' + | 'primaryProps' + | 'skipProps' +>) { return ( -
+ <>
{step.title && ( -

+

{step.title as string}

)} @@ -35,44 +72,140 @@ export function TourTooltip({
{!isLastStep && ( - + )}
{index > 0 && ( - + )} {continuous ? ( - + ) : ( - + )}
-
+ + ) +} + +export function TourTooltip({ + continuous, + index, + step, + backProps, + closeProps, + primaryProps, + skipProps, + isLastStep, + tooltipProps, +}: TooltipRenderProps) { + const [targetEl, setTargetEl] = useState(null) + const hasSetRef = useRef(false) + + useEffect(() => { + hasSetRef.current = false + const { target } = step + if (typeof target === 'string') { + setTargetEl(document.querySelector(target)) + } else if (target instanceof HTMLElement) { + setTargetEl(target) + } else { + setTargetEl(null) + } + }, [step.target]) + + const { side, align } = mapPlacement(step.placement) + const isCentered = step.placement === 'center' + + const refCallback = (node: HTMLDivElement | null) => { + if (!hasSetRef.current && tooltipProps.ref) { + tooltipProps.ref(node) + hasSetRef.current = true + } + } + + const bodyProps = { + step, + continuous, + index, + isLastStep, + backProps, + closeProps, + primaryProps, + skipProps, + } + + const refDiv = ( +
+ ) + + if (!targetEl) { + return refDiv + } + + if (isCentered) { + return ( + <> + {refDiv} + {createPortal( +
+
+ +
+
, + document.body + )} + + ) + } + + return ( + <> + {refDiv} + {createPortal( + + + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > + + + , + document.body + )} + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 07bd5a38922..aaf4146f03d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -36,6 +36,7 @@ import { import { useSession } from '@/lib/auth/auth-client' import { cn } from '@/lib/core/utils/cn' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' +import { START_TOUR_EVENT } from '@/app/workspace/[workspaceId]/components/product-tour' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' @@ -613,7 +614,7 @@ export const Sidebar = memo(function Sidebar() { ) const handleStartTour = useCallback(() => { - window.dispatchEvent(new CustomEvent('start-product-tour')) + window.dispatchEvent(new CustomEvent(START_TOUR_EVENT)) }, []) const { data: fetchedTasks = [], isLoading: tasksLoading } = useTasks(workspaceId) diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index 8702c41a5ac..f73be6400af 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -414,6 +414,18 @@ export interface PopoverContentProps * @default true */ avoidCollisions?: boolean + /** + * Show an arrow pointing toward the anchor element. + * The arrow color matches the popover background based on the current color scheme. + * @default false + */ + showArrow?: boolean + /** + * Custom className for the arrow element. + * Overrides the default color-scheme-based fill when provided. + * Useful when the popover background is overridden via className. + */ + arrowClassName?: string } /** @@ -438,6 +450,8 @@ const PopoverContent = React.forwardRef< collisionPadding = 8, border = false, avoidCollisions = true, + showArrow = false, + arrowClassName, onOpenAutoFocus, onCloseAutoFocus, ...restProps @@ -592,7 +606,8 @@ const PopoverContent = React.forwardRef< onCloseAutoFocus={handleCloseAutoFocus} {...restProps} className={cn( - 'z-[10000200] flex flex-col overflow-auto outline-none will-change-transform', + 'z-[10000200] flex flex-col outline-none will-change-transform', + showArrow ? 'overflow-visible' : 'overflow-auto', STYLES.colorScheme[colorScheme].content, STYLES.content, hasUserWidthConstraint && '[&_.flex-1]:truncate [&_[data-popover-section]]:truncate', @@ -614,6 +629,27 @@ const PopoverContent = React.forwardRef< }} > {children} + {showArrow && ( + + + + + + + )} ) diff --git a/apps/sim/package.json b/apps/sim/package.json index a5cb1161322..c588585b3dd 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -227,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 f12f246837f..fd6a8bf29d4 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)', }, }, }, From 8363177ef7cac487ec0b824a7e687481b6237a9e Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Fri, 20 Mar 2026 00:05:34 +0530 Subject: [PATCH 03/19] chore: fix the tour --- .../components/product-tour/index.ts | 3 +- .../components/product-tour/nav-tour-steps.ts | 66 ++++ .../components/product-tour/product-tour.tsx | 315 +++++++++--------- .../components/product-tour/tour-steps.ts | 107 ------ .../components/product-tour/tour-tooltip.tsx | 211 ------------ .../components/product-tour/use-tour.ts | 245 ++++++++++++++ .../product-tour/workflow-tour-steps.ts | 60 ++++ .../components/product-tour/workflow-tour.tsx | 197 +++++++++++ .../app/workspace/[workspaceId]/layout.tsx | 4 +- .../components/command-list/command-list.tsx | 2 +- .../panel/components/editor/editor.tsx | 2 +- .../[workspaceId]/w/[workflowId]/layout.tsx | 4 + .../w/components/sidebar/sidebar.tsx | 4 +- apps/sim/components/emcn/components/index.ts | 5 + .../components/tour-tooltip/tour-tooltip.tsx | 227 +++++++++++++ apps/sim/tailwind.config.ts | 5 + 16 files changed, 982 insertions(+), 475 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx create mode 100644 apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts index 7713bf725fa..d1aca2af4ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts @@ -1 +1,2 @@ -export { ProductTour, resetTourCompletion, START_TOUR_EVENT } from './product-tour' +export { NavTour } 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 00000000000..02f3743f4c8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts @@ -0,0 +1,66 @@ +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, + }, + { + 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, + }, + { + 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. Agents use these to answer questions with your own data.', + placement: 'right', + disableBeacon: true, + }, + { + target: '[data-item-id="scheduled-tasks"]', + title: 'Scheduled Tasks', + content: + 'View and manage workflows running on a schedule. Monitor upcoming and 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.', + 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.', + 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 index c25d71c16f1..41504e55548 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -1,181 +1,196 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import dynamic from 'next/dynamic' -import { ACTIONS, type CallBackProps, EVENTS, STATUS } from 'react-joyride' -import { tourSteps } from './tour-steps' -import { TourTooltip } from './tour-tooltip' +import type { TooltipRenderProps } from 'react-joyride' +import { TourTooltip } from '@/components/emcn' +import { navTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps' +import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' -const logger = createLogger('ProductTour') +const logger = createLogger('NavTour') const Joyride = dynamic(() => import('react-joyride'), { ssr: false, }) -const TOUR_STORAGE_KEY = 'sim-tour-completed-v1' -export const START_TOUR_EVENT = 'start-product-tour' +const NAV_TOUR_STORAGE_KEY = 'sim-nav-tour-completed-v1' -function isTourCompleted(): boolean { - try { - return localStorage.getItem(TOUR_STORAGE_KEY) === 'true' - } catch { - return false - } +/** Shared state passed from the tour component to the tooltip adapter via context */ +interface TourState { + isTooltipVisible: boolean + isEntrance: boolean + totalSteps: number } -function markTourCompleted(): void { - try { - localStorage.setItem(TOUR_STORAGE_KEY, 'true') - } catch { - logger.warn('Failed to persist tour completion to localStorage') - } -} +const TourStateContext = createContext({ + isTooltipVisible: true, + isEntrance: true, + totalSteps: 0, +}) -export function resetTourCompletion(): void { - try { - localStorage.removeItem(TOUR_STORAGE_KEY) - } catch { - logger.warn('Failed to reset tour completion in localStorage') +/** + * 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' } } -export function ProductTour() { - const [run, setRun] = useState(false) - const [stepIndex, setStepIndex] = useState(0) - const [tourKey, setTourKey] = useState(0) - - const hasAutoStarted = useRef(false) - const retriggerTimerRef = useRef | null>(null) - - useEffect(() => { - if (hasAutoStarted.current) return - hasAutoStarted.current = true - - const timer = setTimeout(() => { - if (!isTourCompleted()) { - setStepIndex(0) - setRun(true) - logger.info('Auto-starting product tour for first-time user') - } - }, 1200) - - return () => clearTimeout(timer) - }, []) +/** + * Adapter that bridges Joyride's tooltip render props to the EMCN TourTooltip component. + * Reads transition state from TourStateContext to coordinate fade animations. + */ +function NavTooltipAdapter({ + step, + index, + isLastStep, + tooltipProps, + primaryProps, + backProps, + closeProps, +}: TooltipRenderProps) { + const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) + const [targetEl, setTargetEl] = useState(null) + const hasSetRef = useRef(false) useEffect(() => { - const handleStartTour = () => { - setRun(false) - resetTourCompletion() - - setTourKey((k) => k + 1) - - if (retriggerTimerRef.current) { - clearTimeout(retriggerTimerRef.current) - } - - retriggerTimerRef.current = setTimeout(() => { - retriggerTimerRef.current = null - setStepIndex(0) - setRun(true) - logger.info('Product tour triggered via custom event') - }, 50) + hasSetRef.current = false + const { target } = step + if (typeof target === 'string') { + setTargetEl(document.querySelector(target)) + } else if (target instanceof HTMLElement) { + setTargetEl(target) + } else { + setTargetEl(null) } + }, [step]) - window.addEventListener(START_TOUR_EVENT, handleStartTour) - return () => { - window.removeEventListener(START_TOUR_EVENT, handleStartTour) - if (retriggerTimerRef.current) { - clearTimeout(retriggerTimerRef.current) + const refCallback = useCallback( + (node: HTMLDivElement | null) => { + if (!hasSetRef.current && tooltipProps.ref) { + ;(tooltipProps.ref as React.RefCallback)(node) + hasSetRef.current = true } - } - }, []) - - const stopTour = useCallback(() => { - setRun(false) - markTourCompleted() - }, []) - - const handleCallback = useCallback( - (data: CallBackProps) => { - const { action, index, status, type } = data - - if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { - stopTour() - logger.info('Product tour ended', { status }) - return - } - - if (type === EVENTS.STEP_AFTER || type === EVENTS.TARGET_NOT_FOUND) { - if (action === ACTIONS.CLOSE) { - stopTour() - logger.info('Product tour closed by user') - return - } - - const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1) + }, + [tooltipProps.ref] + ) - if (type === EVENTS.TARGET_NOT_FOUND) { - logger.info('Tour step target not found, skipping', { - stepIndex: index, - target: tourSteps[index]?.target, - }) - } + const placement = mapPlacement(step.placement) - if (nextIndex >= tourSteps.length || nextIndex < 0) { - stopTour() - return - } + return ( + <> +
+ void} + onBack={backProps.onClick as () => void} + onClose={closeProps.onClick as () => void} + /> + + ) +} - setStepIndex(nextIndex) - } - }, - [stopTour] +/** + * Navigation tour that walks through sidebar items on first workspace visit. + * Runs once automatically and cannot be retriggered. + */ +export function NavTour() { + const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({ + steps: navTourSteps, + storageKey: NAV_TOUR_STORAGE_KEY, + autoStartDelay: 1200, + resettable: false, + tourName: 'Navigation tour', + }) + + const tourState = useMemo( + () => ({ + isTooltipVisible, + isEntrance, + totalSteps: navTourSteps.length, + }), + [isTooltipVisible, isEntrance] ) return ( - + + }} + /> + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts deleted file mode 100644 index 91a683e841a..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-steps.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { Step } from 'react-joyride' - -export const tourSteps: Step[] = [ - { - target: '[data-tour="home-greeting"]', - title: 'Welcome to Sim', - content: - 'This is your home base. From here you can describe what you want to build in plain language, or pick a template to get started.', - placement: 'bottom', - disableBeacon: true, - }, - { - target: '[data-tour="home-chat-input"]', - title: 'Describe your workflow', - content: - 'Type what you want to automate — like "monitor my inbox and summarize new emails." Sim will build an AI workflow for you.', - placement: 'bottom', - disableBeacon: true, - }, - { - target: '[data-tour="home-templates"]', - title: 'Start from a template', - content: - 'Or pick one of these pre-built templates to ship your agent in minutes. Click any card to get started.', - placement: 'top', - disableBeacon: true, - }, - { - target: '.sidebar-container', - title: 'Sidebar navigation', - content: - 'Access everything from here — workflows, tables, files, knowledge base, and logs. This stays with you across all pages.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-item-id="search"]', - title: 'Search anything', - content: 'Use search (or Cmd+K) to quickly find workflows, blocks, tools, and more.', - placement: 'right', - disableBeacon: true, - }, - { - target: '.workflows-section', - title: 'Your workflows', - content: - 'All your workflows live here. Create new ones with the + button, organize with folders, and switch between them.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tour="canvas"]', - title: 'The workflow canvas', - content: - 'This is where you build visually. Drag blocks onto the canvas and connect them together to create AI workflows.', - placement: 'center', - disableBeacon: true, - }, - { - target: '[data-tour="command-list"]', - title: 'Quick actions', - content: - 'Use these keyboard shortcuts to get started fast. Try Cmd+K to search for blocks, or Cmd+Y to browse templates.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tab-button="toolbar"]', - title: 'Block library', - content: - 'The Toolbar is your block library. Drag triggers and blocks onto the canvas to build your workflow step by step.', - placement: 'bottom', - disableBeacon: true, - }, - { - target: '[data-tab-button="copilot"]', - title: 'AI Copilot', - content: - 'Copilot helps you build and debug workflows using natural language. Describe what you want and it creates blocks for you.', - placement: 'bottom', - disableBeacon: true, - }, - { - 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, - }, - { - target: '[data-tour="deploy-run"]', - title: 'Run and deploy', - content: - 'Hit Run to execute your workflow and see results in the terminal below. When ready, Deploy as an API, webhook, schedule, or chat widget.', - placement: 'bottom', - disableBeacon: true, - }, - { - target: '[data-tour="workflow-controls"]', - title: 'Canvas controls', - content: - 'Switch between pointer and hand mode, undo/redo changes, and fit your canvas to view.', - placement: 'top', - disableBeacon: true, - }, -] diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx deleted file mode 100644 index e5c52ac9ee7..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-tooltip.tsx +++ /dev/null @@ -1,211 +0,0 @@ -'use client' - -import { useEffect, useRef, useState } from 'react' -import { createPortal } from 'react-dom' -import type { TooltipRenderProps } from 'react-joyride' -import { Button, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn' - -function mapPlacement(placement?: string): { - side: 'top' | 'right' | 'bottom' | 'left' - align: 'start' | 'center' | 'end' -} { - switch (placement) { - case 'top': - case 'top-start': - return { side: 'top', align: 'center' } - case 'top-end': - return { side: 'top', align: 'end' } - case 'right': - case 'right-start': - return { side: 'right', align: 'center' } - case 'right-end': - return { side: 'right', align: 'end' } - case 'bottom': - case 'bottom-start': - return { side: 'bottom', align: 'center' } - case 'bottom-end': - return { side: 'bottom', align: 'end' } - case 'left': - case 'left-start': - return { side: 'left', align: 'center' } - case 'left-end': - return { side: 'left', align: 'end' } - case 'center': - return { side: 'bottom', align: 'center' } - default: - return { side: 'bottom', align: 'center' } - } -} - -function TourTooltipBody({ - step, - continuous, - index, - isLastStep, - backProps, - closeProps, - primaryProps, - skipProps, -}: Pick< - TooltipRenderProps, - | 'step' - | 'continuous' - | 'index' - | 'isLastStep' - | 'backProps' - | 'closeProps' - | 'primaryProps' - | 'skipProps' ->) { - return ( - <> -
- {step.title && ( -

- {step.title as string} -

- )} -
-
-

{step.content}

-
-
-
- {!isLastStep && ( - - )} -
-
- {index > 0 && ( - - )} - {continuous ? ( - - ) : ( - - )} -
-
- - ) -} - -export function TourTooltip({ - continuous, - index, - step, - backProps, - closeProps, - primaryProps, - skipProps, - isLastStep, - tooltipProps, -}: TooltipRenderProps) { - const [targetEl, setTargetEl] = useState(null) - const hasSetRef = useRef(false) - - useEffect(() => { - hasSetRef.current = false - const { target } = step - if (typeof target === 'string') { - setTargetEl(document.querySelector(target)) - } else if (target instanceof HTMLElement) { - setTargetEl(target) - } else { - setTargetEl(null) - } - }, [step.target]) - - const { side, align } = mapPlacement(step.placement) - const isCentered = step.placement === 'center' - - const refCallback = (node: HTMLDivElement | null) => { - if (!hasSetRef.current && tooltipProps.ref) { - tooltipProps.ref(node) - hasSetRef.current = true - } - } - - const bodyProps = { - step, - continuous, - index, - isLastStep, - backProps, - closeProps, - primaryProps, - skipProps, - } - - const refDiv = ( -
- ) - - if (!targetEl) { - return refDiv - } - - if (isCentered) { - return ( - <> - {refDiv} - {createPortal( -
-
- -
-
, - document.body - )} - - ) - } - - return ( - <> - {refDiv} - {createPortal( - - - e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - > - - - , - document.body - )} - - ) -} 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 00000000000..bef04c4a90c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts @@ -0,0 +1,245 @@ +'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 +} + +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', +}: 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 prevOverflowRef = useRef('') + + /** Lock page scroll to prevent scrollbar jitter from Joyride's overlay */ + const lockScroll = useCallback(() => { + prevOverflowRef.current = document.documentElement.style.overflow + document.documentElement.style.overflow = 'hidden' + }, []) + + const unlockScroll = useCallback(() => { + document.documentElement.style.overflow = prevOverflowRef.current + }, []) + + const stopTour = useCallback(() => { + setRun(false) + setIsTooltipVisible(true) + setIsEntrance(true) + unlockScroll() + markTourCompleted(storageKey) + }, [storageKey, unlockScroll]) + + /** 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 + } + + /** Fade out the current tooltip */ + 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 fading in the tooltip at the new position. + */ + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setIsTooltipVisible(true) + }) + }) + }, FADE_OUT_MS) + }, + [steps.length, stopTour] + ) + + /** Auto-start on first visit */ + useEffect(() => { + if (hasAutoStarted.current) return + hasAutoStarted.current = true + + const timer = setTimeout(() => { + if (!isTourCompleted(storageKey)) { + lockScroll() + setStepIndex(0) + setIsEntrance(true) + setIsTooltipVisible(true) + setRun(true) + logger.info(`Auto-starting ${tourName}`) + } + }, autoStartDelay) + + return () => clearTimeout(timer) + }, [storageKey, autoStartDelay, tourName]) + + /** 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) + } + + retriggerTimerRef.current = setTimeout(() => { + retriggerTimerRef.current = null + lockScroll() + setStepIndex(0) + setIsEntrance(true) + setIsTooltipVisible(true) + setRun(true) + logger.info(`${tourName} triggered via event`) + }, 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) + } + unlockScroll() + } + }, [unlockScroll]) + + 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 00000000000..dbcfade9819 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts @@ -0,0 +1,60 @@ +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-tour="command-list"]', + title: 'Quick Actions', + content: + 'Keyboard shortcuts to get started fast. Press Cmd+K to search blocks, or Cmd+Y to browse templates.', + placement: 'right', + disableBeacon: true, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, + { + target: '[data-tour="deploy-run"]', + title: 'Run & Deploy', + content: + 'Hit Run to test your workflow. When ready, Deploy it as an API, webhook, schedule, or chat widget.', + 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', + 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 00000000000..9d3006ea73c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx @@ -0,0 +1,197 @@ +'use client' + +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' +import dynamic from 'next/dynamic' +import type { TooltipRenderProps } from 'react-joyride' +import { TourTooltip } from '@/components/emcn' +import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' +import { workflowTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps' + +const logger = createLogger('WorkflowTour') + +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' + +/** Shared state passed from the tour component to the tooltip adapter via context */ +interface TourState { + isTooltipVisible: boolean + isEntrance: boolean + totalSteps: number +} + +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. + */ +function WorkflowTooltipAdapter({ + step, + index, + isLastStep, + tooltipProps, + primaryProps, + backProps, + closeProps, +}: TooltipRenderProps) { + const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) + const [targetEl, setTargetEl] = useState(null) + const hasSetRef = useRef(false) + + useEffect(() => { + hasSetRef.current = false + 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 (!hasSetRef.current && tooltipProps.ref) { + ;(tooltipProps.ref as React.RefCallback)(node) + hasSetRef.current = true + } + }, + [tooltipProps.ref] + ) + + const placement = mapPlacement(step.placement) + + return ( + <> +
+ void} + onBack={backProps.onClick as () => void} + onClose={closeProps.onClick as () => void} + /> + + ) +} + +/** + * 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]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index da884b92573..dba9198ba50 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -1,5 +1,5 @@ import { ToastProvider } from '@/components/emcn' -import { ProductTour } from '@/app/workspace/[workspaceId]/components/product-tour' +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' @@ -22,7 +22,7 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod {children}
- +
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 83f8e0ae20c..6fe1eb3b9a5 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 @@ -174,12 +174,12 @@ export function CommandList() { return (
{/* Header */} -
+
{(blockConfig || isSubflow) && currentBlock?.type !== 'note' && (
{children} + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index aaf4146f03d..ebb6a6ecab6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -36,7 +36,7 @@ import { import { useSession } from '@/lib/auth/auth-client' import { cn } from '@/lib/core/utils/cn' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' -import { START_TOUR_EVENT } from '@/app/workspace/[workspaceId]/components/product-tour' +import { START_WORKFLOW_TOUR_EVENT } from '@/app/workspace/[workspaceId]/components/product-tour' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' @@ -614,7 +614,7 @@ export const Sidebar = memo(function Sidebar() { ) const handleStartTour = useCallback(() => { - window.dispatchEvent(new CustomEvent(START_TOUR_EVENT)) + window.dispatchEvent(new CustomEvent(START_WORKFLOW_TOUR_EVENT)) }, []) const { data: fetchedTasks = [], isLoading: tasksLoading } = useTasks(workspaceId) diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts index 1f19d7d4b72..30b4aa349ca 100644 --- a/apps/sim/components/emcn/components/index.ts +++ b/apps/sim/components/emcn/components/index.ts @@ -147,3 +147,8 @@ export { TimePicker, type TimePickerProps, timePickerVariants } from './time-pic export { CountdownRing } from './toast/countdown-ring' export { ToastProvider, toast, useToast } from './toast/toast' export { Tooltip } from './tooltip/tooltip' +export { + TourTooltip, + type TourTooltipPlacement, + type TourTooltipProps, +} from './tour-tooltip/tour-tooltip' 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 00000000000..3a3de2ac5b2 --- /dev/null +++ b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx @@ -0,0 +1,227 @@ +'use client' + +import type * as React from 'react' +import * as PopoverPrimitive from '@radix-ui/react-popover' +import { createPortal } from 'react-dom' +import { Button } from '@/components/emcn/components/button/button' +import { X } from '@/components/emcn/icons' +import { cn } from '@/lib/core/utils/cn' + +type TourTooltipPlacement = 'top' | 'right' | 'bottom' | 'left' | 'center' + +interface TourTooltipProps { + /** Title displayed at the top of the tooltip */ + title: string + /** Description text below the title */ + description: React.ReactNode + /** Current step number (1-based) */ + step: number + /** Total number of steps in the tour */ + totalSteps: number + /** Placement relative to the target element */ + placement?: TourTooltipPlacement + /** Target DOM element to anchor the tooltip to */ + targetEl: HTMLElement | null + /** Whether this is the first step (hides Back button visually) */ + isFirst?: boolean + /** Whether this is the last step (changes Next to Done) */ + isLast?: boolean + /** Controls tooltip visibility for smooth transitions */ + isVisible?: boolean + /** Whether this is the initial entrance (plays full entrance animation) */ + isEntrance?: 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 + /** 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', +} + +/** + * Inner card content rendered inside the tooltip. + * Separated for reuse between positioned and centered layouts. + */ +function TourTooltipCard({ + title, + description, + step, + totalSteps, + isFirst, + isLast, + onNext, + onBack, + onClose, +}: Pick< + TourTooltipProps, + | 'title' + | 'description' + | 'step' + | 'totalSteps' + | 'isFirst' + | 'isLast' + | 'onNext' + | 'onBack' + | 'onClose' +>) { + return ( + <> +
+

+ {title} +

+ +
+ +
+

{description}

+
+ +
+ + {step} / {totalSteps} + +
+
+ +
+ +
+
+ + ) +} + +/** + * 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. + * + * @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 + + const isCentered = placement === 'center' + + const cardClasses = cn( + 'w-[300px] rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)]', + 'shadow-[0_4px_16px_rgba(0,0,0,0.12)]', + 'transition-opacity duration-[80ms] ease-out', + isVisible ? 'opacity-100' : 'opacity-0', + isEntrance && isVisible && '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 { TourTooltip } +export type { TourTooltipProps, TourTooltipPlacement } diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts index fd6a8bf29d4..16e8d421ebd 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -184,6 +184,10 @@ export default { from: { opacity: '0', transform: 'scale(0.96) translateY(4px)' }, to: { opacity: '1', transform: 'scale(1) translateY(0)' }, }, + 'tour-tooltip-fade': { + from: { opacity: '0' }, + to: { opacity: '1' }, + }, }, animation: { 'caret-blink': 'caret-blink 1.25s ease-out infinite', @@ -198,6 +202,7 @@ export default { '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)', + 'tour-tooltip-fade': 'tour-tooltip-fade 150ms cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, }, }, From baa679907e1c006d426ae7943700933c86c10194 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sat, 21 Mar 2026 07:02:44 +0530 Subject: [PATCH 04/19] chore: Tour Updates --- .../components/product-tour/nav-tour-steps.ts | 2 + .../components/product-tour/product-tour.tsx | 7 +- .../components/product-tour/use-tour.ts | 44 ++++--- .../product-tour/workflow-tour-steps.ts | 24 ++-- .../components/product-tour/workflow-tour.tsx | 10 +- .../[workspaceId]/w/[workflowId]/layout.tsx | 4 +- apps/sim/components/emcn/components/index.ts | 2 + .../components/tour-tooltip/tour-tooltip.tsx | 121 +++++++++--------- 8 files changed, 111 insertions(+), 103 deletions(-) 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 index 02f3743f4c8..0ce4af4737f 100644 --- 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 @@ -8,6 +8,7 @@ export const navTourSteps: Step[] = [ '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"]', @@ -15,6 +16,7 @@ export const navTourSteps: Step[] = [ 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"]', 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 index 41504e55548..f9f6cd24162 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -157,6 +157,7 @@ export function NavTour() { callback={handleCallback} continuous disableScrolling + disableScrollParentFix disableOverlayClose spotlightPadding={4} tooltipComponent={NavTooltipAdapter} @@ -180,14 +181,18 @@ export function NavTour() { spotlight: { backgroundColor: 'transparent', border: '1px solid rgba(255, 255, 255, 0.1)', - borderRadius: 6, + borderRadius: 8, boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.55)', + position: 'fixed' as React.CSSProperties['position'], 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)', }, overlay: { backgroundColor: 'transparent', mixBlendMode: 'unset' as React.CSSProperties['mixBlendMode'], + position: 'fixed' as React.CSSProperties['position'], + height: '100%', + overflow: 'visible', }, }} /> 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 index bef04c4a90c..13fc7ffab3f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts @@ -87,25 +87,13 @@ export function useTour({ const hasAutoStarted = useRef(false) const retriggerTimerRef = useRef | null>(null) const transitionTimerRef = useRef | null>(null) - const prevOverflowRef = useRef('') - - /** Lock page scroll to prevent scrollbar jitter from Joyride's overlay */ - const lockScroll = useCallback(() => { - prevOverflowRef.current = document.documentElement.style.overflow - document.documentElement.style.overflow = 'hidden' - }, []) - - const unlockScroll = useCallback(() => { - document.documentElement.style.overflow = prevOverflowRef.current - }, []) const stopTour = useCallback(() => { setRun(false) setIsTooltipVisible(true) setIsEntrance(true) - unlockScroll() markTourCompleted(storageKey) - }, [storageKey, unlockScroll]) + }, [storageKey]) /** Transition to a new step with a coordinated fade-out/fade-in */ const transitionToStep = useCallback( @@ -115,7 +103,7 @@ export function useTour({ return } - /** Fade out the current tooltip */ + /** Hide tooltip during transition */ setIsTooltipVisible(false) if (transitionTimerRef.current) { @@ -129,7 +117,7 @@ export function useTour({ /** * Wait for the browser to process the Radix Popover repositioning - * before fading in the tooltip at the new position. + * before showing the tooltip at the new position. */ requestAnimationFrame(() => { requestAnimationFrame(() => { @@ -148,12 +136,17 @@ export function useTour({ const timer = setTimeout(() => { if (!isTourCompleted(storageKey)) { - lockScroll() setStepIndex(0) setIsEntrance(true) - setIsTooltipVisible(true) + setIsTooltipVisible(false) setRun(true) logger.info(`Auto-starting ${tourName}`) + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setIsTooltipVisible(true) + }) + }) } }, autoStartDelay) @@ -173,14 +166,24 @@ 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 - lockScroll() setStepIndex(0) setIsEntrance(true) - setIsTooltipVisible(true) + setIsTooltipVisible(false) setRun(true) logger.info(`${tourName} triggered via event`) + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setIsTooltipVisible(true) + }) + }) }, 50) } @@ -198,9 +201,8 @@ export function useTour({ if (transitionTimerRef.current) { clearTimeout(transitionTimerRef.current) } - unlockScroll() } - }, [unlockScroll]) + }, []) const handleCallback = useCallback( (data: CallBackProps) => { 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 index dbcfade9819..4a425558696 100644 --- 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 @@ -10,12 +10,13 @@ export const workflowTourSteps: Step[] = [ disableBeacon: true, }, { - target: '[data-tour="command-list"]', - title: 'Quick Actions', + target: '[data-tab-button="copilot"]', + title: 'AI Copilot', content: - 'Keyboard shortcuts to get started fast. Press Cmd+K to search blocks, or Cmd+Y to browse templates.', - placement: 'right', + '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"]', @@ -24,14 +25,7 @@ export const workflowTourSteps: Step[] = [ 'Browse all available blocks and triggers. Drag them onto the canvas to build your workflow step by step.', placement: 'bottom', 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="editor"]', @@ -40,12 +34,13 @@ export const workflowTourSteps: Step[] = [ '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: 'Run & Deploy', + title: 'Deploy & Run', content: - 'Hit Run to test your workflow. When ready, Deploy it as an API, webhook, schedule, or chat widget.', + 'Deploy your workflow as an API, webhook, schedule, or chat widget. Then hit Run to test it out.', placement: 'bottom', disableBeacon: true, }, @@ -55,6 +50,7 @@ export const workflowTourSteps: Step[] = [ 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 index 9d3006ea73c..b0964f1c6a9 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx @@ -1,15 +1,12 @@ 'use client' import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' -import { createLogger } from '@sim/logger' import dynamic from 'next/dynamic' import type { TooltipRenderProps } from 'react-joyride' import { TourTooltip } from '@/components/emcn' import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' import { workflowTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps' -const logger = createLogger('WorkflowTour') - const Joyride = dynamic(() => import('react-joyride'), { ssr: false, }) @@ -158,8 +155,9 @@ export function WorkflowTour() { callback={handleCallback} continuous disableScrolling + disableScrollParentFix disableOverlayClose - spotlightPadding={4} + spotlightPadding={1} tooltipComponent={WorkflowTooltipAdapter} floaterProps={{ disableAnimation: true, @@ -183,12 +181,16 @@ export function WorkflowTour() { border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: 6, boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.55)', + position: 'fixed' as React.CSSProperties['position'], 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)', }, overlay: { backgroundColor: 'transparent', mixBlendMode: 'unset' as React.CSSProperties['mixBlendMode'], + position: 'fixed' as React.CSSProperties['position'], + height: '100%', + overflow: 'visible', }, }} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx index 7da2877ecf0..d69fd3f9ed2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx @@ -7,7 +7,9 @@ export default function WorkflowLayout({ children }: { children: React.ReactNode return (
{children} - +
+ +
) } diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts index 30b4aa349ca..2a93e4ade91 100644 --- a/apps/sim/components/emcn/components/index.ts +++ b/apps/sim/components/emcn/components/index.ts @@ -148,6 +148,8 @@ export { CountdownRing } from './toast/countdown-ring' export { ToastProvider, toast, useToast } from './toast/toast' export { Tooltip } from './tooltip/tooltip' export { + TourCard, + type TourCardProps, TourTooltip, type TourTooltipPlacement, type TourTooltipProps, diff --git a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx index 3a3de2ac5b2..b35f09e6694 100644 --- a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx +++ b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx @@ -2,59 +2,35 @@ 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 { X } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' type TourTooltipPlacement = 'top' | 'right' | 'bottom' | 'left' | 'center' -interface TourTooltipProps { - /** Title displayed at the top of the tooltip */ +interface TourCardProps { + /** Title displayed in the card header */ title: string - /** Description text below the title */ + /** 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 - /** Placement relative to the target element */ - placement?: TourTooltipPlacement - /** Target DOM element to anchor the tooltip to */ - targetEl: HTMLElement | null - /** Whether this is the first step (hides Back button visually) */ + /** Whether this is the first step (hides Back button) */ isFirst?: boolean /** Whether this is the last step (changes Next to Done) */ isLast?: boolean - /** Controls tooltip visibility for smooth transitions */ - isVisible?: boolean - /** Whether this is the initial entrance (plays full entrance animation) */ - isEntrance?: 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 - /** 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', -} - -/** - * Inner card content rendered inside the tooltip. - * Separated for reuse between positioned and centered layouts. - */ -function TourTooltipCard({ +function TourCard({ title, description, step, @@ -64,40 +40,29 @@ function TourTooltipCard({ onNext, onBack, onClose, -}: Pick< - TourTooltipProps, - | 'title' - | 'description' - | 'step' - | 'totalSteps' - | 'isFirst' - | 'isLast' - | 'onNext' - | 'onBack' - | 'onClose' ->) { +}: TourCardProps) { return ( <> -
-

+
+

{title}

-
+

{description}

-
+
{step} / {totalSteps} @@ -116,11 +81,35 @@ function TourTooltipCard({ ) } +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 @@ -153,20 +142,18 @@ function TourTooltip({ className, }: TourTooltipProps) { if (typeof document === 'undefined') return null + if (!isVisible) return null const isCentered = placement === 'center' const cardClasses = cn( - 'w-[300px] rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)]', - 'shadow-[0_4px_16px_rgba(0,0,0,0.12)]', - 'transition-opacity duration-[80ms] ease-out', - isVisible ? 'opacity-100' : 'opacity-0', - isEntrance && isVisible && 'animate-tour-tooltip-in motion-reduce:animate-none', + 'w-[300px] overflow-hidden rounded-[8px] bg-[var(--bg)]', + isEntrance && 'animate-tour-tooltip-in motion-reduce:animate-none', className ) const cardContent = ( - -
{cardContent}
+
+
+
+ {cardContent} +
, document.body ) @@ -200,6 +195,9 @@ function TourTooltip({ collisionPadding={12} avoidCollisions className='z-[10000300] outline-none' + style={{ + filter: 'drop-shadow(0 0 0.5px var(--border)) drop-shadow(0 1px 2px rgba(0,0,0,0.1))', + }} onOpenAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} > @@ -210,10 +208,9 @@ function TourTooltip({ height={7} viewBox='0 0 14 7' preserveAspectRatio='none' - className='fill-[var(--surface-1)] stroke-[var(--border)]' + className='-mt-px fill-[var(--bg)]' > - - + @@ -223,5 +220,5 @@ function TourTooltip({ ) } -export { TourTooltip } -export type { TourTooltipProps, TourTooltipPlacement } +export { TourCard, TourTooltip } +export type { TourCardProps, TourTooltipPlacement, TourTooltipProps } From 43a33d09e904ca0ca870f88f61c24d159ba64162 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sat, 21 Mar 2026 16:20:25 +0530 Subject: [PATCH 05/19] chore: fix review changes --- .../app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index ebb6a6ecab6..521990d0b44 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -1301,7 +1301,6 @@ export const Sidebar = memo(function Sidebar() { className='h-[18px] w-[18px] rounded-[4px] p-0 hover:bg-[var(--surface-active)]' onClick={handleCreateWorkflow} disabled={isCreatingWorkflow || !canEdit} - data-tour='new-workflow' > From 73b0f87101c3ee601c0829f26e46cf8cc77d7712 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sat, 21 Mar 2026 16:23:37 +0530 Subject: [PATCH 06/19] chore: fix review changes --- .../[workflowId]/components/panel/components/editor/editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 95af05f41cb..4a7bc682f68 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -319,7 +319,7 @@ export function Editor() { return (
{/* Header */} -
+
{(blockConfig || isSubflow) && currentBlock?.type !== 'note' && (
Date: Sat, 21 Mar 2026 16:23:47 +0530 Subject: [PATCH 07/19] chore: fix review changes --- .../components/emcn/components/tour-tooltip/tour-tooltip.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx index b35f09e6694..42a693a541a 100644 --- a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx +++ b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx @@ -62,7 +62,7 @@ function TourCard({

{description}

-
+
{step} / {totalSteps} @@ -173,7 +173,7 @@ function TourTooltip({
{cardContent} From 0522b2cbe999802bf690ea26a541719bbf2582da Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sat, 21 Mar 2026 16:24:27 +0530 Subject: [PATCH 08/19] chore: fix review changes --- apps/sim/components/emcn/components/popover/popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index f73be6400af..6672fc5123b 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -640,7 +640,7 @@ const PopoverContent = React.forwardRef< arrowClassName ?? cn( colorScheme === 'inverted' - ? 'fill-[##242424] stroke-[#363636] dark:fill-[var(--surface-3)] dark:stroke-[var(--border-1)]' + ? 'fill-[#242424] stroke-[#363636] dark:fill-[var(--surface-3)] dark:stroke-[var(--border-1)]' : 'fill-[var(--surface-3)] stroke-[var(--border-1)] dark:fill-[var(--surface-3)]' ) } From f9a52fb41992167f0a36acde7e1e4966a76268b1 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sat, 21 Mar 2026 16:32:24 +0530 Subject: [PATCH 09/19] chore: fix review changes --- .../components/product-tour/product-tour.tsx | 9 +++++---- .../[workspaceId]/components/product-tour/use-tour.ts | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) 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 index f9f6cd24162..680cd976ffe 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -3,6 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import dynamic from 'next/dynamic' +import { usePathname } from 'next/navigation' import type { TooltipRenderProps } from 'react-joyride' import { TourTooltip } from '@/components/emcn' import { navTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps' @@ -125,17 +126,17 @@ function NavTooltipAdapter({ ) } -/** - * Navigation tour that walks through sidebar items on first workspace visit. - * Runs once automatically and cannot be retriggered. - */ 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: false, tourName: 'Navigation tour', + disabled: isWorkflowPage, }) const tourState = useMemo( 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 index 13fc7ffab3f..e57f5537b66 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts @@ -22,6 +22,8 @@ interface UseTourOptions { 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 { @@ -77,6 +79,7 @@ export function useTour({ resettable = false, triggerEvent, tourName = 'tour', + disabled = false, }: UseTourOptions): UseTourReturn { const [run, setRun] = useState(false) const [stepIndex, setStepIndex] = useState(0) @@ -131,7 +134,7 @@ export function useTour({ /** Auto-start on first visit */ useEffect(() => { - if (hasAutoStarted.current) return + if (disabled || hasAutoStarted.current) return hasAutoStarted.current = true const timer = setTimeout(() => { @@ -151,7 +154,7 @@ export function useTour({ }, autoStartDelay) return () => clearTimeout(timer) - }, [storageKey, autoStartDelay, tourName]) + }, [storageKey, autoStartDelay, tourName, disabled]) /** Listen for manual trigger events */ useEffect(() => { From bca0eceeec00087b60c41071bcc9a70ccc61fb49 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 21 Mar 2026 12:15:04 -0700 Subject: [PATCH 10/19] minor improvements --- .../components/product-tour/index.ts | 2 +- .../components/product-tour/nav-tour-steps.ts | 16 ++++++++++++---- .../components/product-tour/product-tour.tsx | 5 ++++- .../components/product-tour/workflow-tour.tsx | 1 + .../workflow-controls/workflow-controls.tsx | 2 +- .../w/components/sidebar/sidebar.tsx | 11 ++++++++--- .../components/tour-tooltip/tour-tooltip.tsx | 2 +- 7 files changed, 28 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts index d1aca2af4ed..38779f28cbb 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts @@ -1,2 +1,2 @@ -export { NavTour } from './product-tour' +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 index 0ce4af4737f..632d94357d6 100644 --- 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 @@ -37,7 +37,7 @@ export const navTourSteps: Step[] = [ target: '[data-item-id="knowledge-base"]', title: 'Knowledge Base', content: - 'Build knowledge bases from your documents. Agents use these to answer questions with your own data.', + '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, }, @@ -45,7 +45,7 @@ export const navTourSteps: Step[] = [ target: '[data-item-id="scheduled-tasks"]', title: 'Scheduled Tasks', content: - 'View and manage workflows running on a schedule. Monitor upcoming and past executions.', + '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, }, @@ -53,7 +53,15 @@ export const navTourSteps: Step[] = [ target: '[data-item-id="logs"]', title: 'Logs', content: - 'Monitor every workflow execution. See inputs, outputs, errors, and timing for each run.', + '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, }, @@ -61,7 +69,7 @@ export const navTourSteps: Step[] = [ target: '.workflows-section', title: 'Workflows', content: - 'All your workflows live here. Create new ones with the + button and organize them into folders.', + '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 index 680cd976ffe..bc9d3594ccc 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -16,6 +16,7 @@ const Joyride = dynamic(() => import('react-joyride'), { }) const NAV_TOUR_STORAGE_KEY = 'sim-nav-tour-completed-v1' +export const START_NAV_TOUR_EVENT = 'start-nav-tour' /** Shared state passed from the tour component to the tooltip adapter via context */ interface TourState { @@ -134,7 +135,8 @@ export function NavTour() { steps: navTourSteps, storageKey: NAV_TOUR_STORAGE_KEY, autoStartDelay: 1200, - resettable: false, + resettable: true, + triggerEvent: START_NAV_TOUR_EVENT, tourName: 'Navigation tour', disabled: isWorkflowPage, }) @@ -194,6 +196,7 @@ export function NavTour() { position: 'fixed' as React.CSSProperties['position'], height: '100%', overflow: 'visible', + pointerEvents: 'none' as React.CSSProperties['pointerEvents'], }, }} /> 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 index b0964f1c6a9..e782791a441 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx @@ -191,6 +191,7 @@ export function WorkflowTour() { position: 'fixed' as React.CSSProperties['position'], height: '100%', overflow: 'visible', + pointerEvents: 'none' as React.CSSProperties['pointerEvents'], }, }} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx index d98465cbb67..44942c64c2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx @@ -80,7 +80,7 @@ export const WorkflowControls = memo(function WorkflowControls() { } if (!showWorkflowControls) { - return null + return
} return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 521990d0b44..2e3cf0d6498 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -36,7 +36,10 @@ import { import { useSession } from '@/lib/auth/auth-client' import { cn } from '@/lib/core/utils/cn' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' -import { START_WORKFLOW_TOUR_EVENT } from '@/app/workspace/[workspaceId]/components/product-tour' +import { + START_NAV_TOUR_EVENT, + START_WORKFLOW_TOUR_EVENT, +} from '@/app/workspace/[workspaceId]/components/product-tour' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' @@ -614,7 +617,9 @@ export const Sidebar = memo(function Sidebar() { ) const handleStartTour = useCallback(() => { - window.dispatchEvent(new CustomEvent(START_WORKFLOW_TOUR_EVENT)) + window.dispatchEvent( + new CustomEvent(isOnWorkflowPage ? START_WORKFLOW_TOUR_EVENT : START_NAV_TOUR_EVENT) + ) }, []) const { data: fetchedTasks = [], isLoading: tasksLoading } = useTasks(workspaceId) @@ -1133,7 +1138,7 @@ export const Sidebar = memo(function Sidebar() { )} > {/* Tasks */} -
+
All tasks
{!isCollapsed && ( diff --git a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx index 42a693a541a..34f76fe99b9 100644 --- a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx +++ b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx @@ -147,7 +147,7 @@ function TourTooltip({ const isCentered = placement === 'center' const cardClasses = cn( - 'w-[300px] overflow-hidden rounded-[8px] bg-[var(--bg)]', + 'w-[260px] overflow-hidden rounded-[8px] bg-[var(--bg)]', isEntrance && 'animate-tour-tooltip-in motion-reduce:animate-none', className ) From d2431207e8a275bfaafd545c16a7b5e3a7eed15e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 21 Mar 2026 12:25:04 -0700 Subject: [PATCH 11/19] chore(tour): address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract shared TourState, TourStateContext, mapPlacement, and TourTooltipAdapter into tour-shared.tsx, eliminating ~100 lines of duplication between product-tour.tsx and workflow-tour.tsx - Fix stale closure in handleStartTour — add isOnWorkflowPage to useCallback deps so Take a tour dispatches the correct event after navigation --- .../components/product-tour/product-tour.tsx | 120 +----------------- .../components/product-tour/tour-shared.tsx | 114 +++++++++++++++++ .../components/product-tour/workflow-tour.tsx | 119 +---------------- .../w/components/sidebar/sidebar.tsx | 2 +- 4 files changed, 129 insertions(+), 226 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx 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 index bc9d3594ccc..3a2ea168b7a 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -1,12 +1,15 @@ 'use client' -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { useMemo } from 'react' import { createLogger } from '@sim/logger' import dynamic from 'next/dynamic' import { usePathname } from 'next/navigation' -import type { TooltipRenderProps } from 'react-joyride' -import { TourTooltip } from '@/components/emcn' 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 { + TourStateContext, + TourTooltipAdapter, +} from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' const logger = createLogger('NavTour') @@ -18,115 +21,6 @@ const Joyride = dynamic(() => import('react-joyride'), { const NAV_TOUR_STORAGE_KEY = 'sim-nav-tour-completed-v1' export const START_NAV_TOUR_EVENT = 'start-nav-tour' -/** Shared state passed from the tour component to the tooltip adapter via context */ -interface TourState { - isTooltipVisible: boolean - isEntrance: boolean - totalSteps: number -} - -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. - */ -function NavTooltipAdapter({ - step, - index, - isLastStep, - tooltipProps, - primaryProps, - backProps, - closeProps, -}: TooltipRenderProps) { - const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) - const [targetEl, setTargetEl] = useState(null) - const hasSetRef = useRef(false) - - useEffect(() => { - hasSetRef.current = false - 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 (!hasSetRef.current && tooltipProps.ref) { - ;(tooltipProps.ref as React.RefCallback)(node) - hasSetRef.current = true - } - }, - [tooltipProps.ref] - ) - - const placement = mapPlacement(step.placement) - - return ( - <> -
- void} - onBack={backProps.onClick as () => void} - onClose={closeProps.onClick as () => void} - /> - - ) -} - export function NavTour() { const pathname = usePathname() const isWorkflowPage = /\/w\/[^/]+/.test(pathname) @@ -163,7 +57,7 @@ export function NavTour() { disableScrollParentFix disableOverlayClose spotlightPadding={4} - tooltipComponent={NavTooltipAdapter} + tooltipComponent={TourTooltipAdapter} floaterProps={{ disableAnimation: true, hideArrow: true, 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 00000000000..44bbe5179cf --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx @@ -0,0 +1,114 @@ +'use client' + +import { createContext, useCallback, useContext, useEffect, useRef, 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) + const hasSetRef = useRef(false) + + useEffect(() => { + hasSetRef.current = false + 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 (!hasSetRef.current && tooltipProps.ref) { + ;(tooltipProps.ref as React.RefCallback)(node) + hasSetRef.current = true + } + }, + [tooltipProps.ref] + ) + + const placement = mapPlacement(step.placement) + + return ( + <> +
+ void} + onBack={backProps.onClick as () => void} + onClose={closeProps.onClick as () => void} + /> + + ) +} 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 index e782791a441..6bb55a8cb2a 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx @@ -1,9 +1,12 @@ 'use client' -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { useMemo } from 'react' import dynamic from 'next/dynamic' -import type { TooltipRenderProps } from 'react-joyride' -import { TourTooltip } from '@/components/emcn' +import type { TourState } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' +import { + 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' @@ -14,114 +17,6 @@ const Joyride = dynamic(() => import('react-joyride'), { const WORKFLOW_TOUR_STORAGE_KEY = 'sim-workflow-tour-completed-v1' export const START_WORKFLOW_TOUR_EVENT = 'start-workflow-tour' -/** Shared state passed from the tour component to the tooltip adapter via context */ -interface TourState { - isTooltipVisible: boolean - isEntrance: boolean - totalSteps: number -} - -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. - */ -function WorkflowTooltipAdapter({ - step, - index, - isLastStep, - tooltipProps, - primaryProps, - backProps, - closeProps, -}: TooltipRenderProps) { - const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) - const [targetEl, setTargetEl] = useState(null) - const hasSetRef = useRef(false) - - useEffect(() => { - hasSetRef.current = false - 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 (!hasSetRef.current && tooltipProps.ref) { - ;(tooltipProps.ref as React.RefCallback)(node) - hasSetRef.current = true - } - }, - [tooltipProps.ref] - ) - - const placement = mapPlacement(step.placement) - - return ( - <> -
- void} - onBack={backProps.onClick as () => void} - onClose={closeProps.onClick as () => void} - /> - - ) -} - /** * Workflow tour that covers the canvas, blocks, copilot, and deployment. * Runs on first workflow visit and can be retriggered via "Take a tour". @@ -158,7 +53,7 @@ export function WorkflowTour() { disableScrollParentFix disableOverlayClose spotlightPadding={1} - tooltipComponent={WorkflowTooltipAdapter} + tooltipComponent={TourTooltipAdapter} floaterProps={{ disableAnimation: true, hideArrow: true, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 2e3cf0d6498..b8790653c0b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -620,7 +620,7 @@ export const Sidebar = memo(function Sidebar() { window.dispatchEvent( new CustomEvent(isOnWorkflowPage ? START_WORKFLOW_TOUR_EVENT : START_NAV_TOUR_EVENT) ) - }, []) + }, [isOnWorkflowPage]) const { data: fetchedTasks = [], isLoading: tasksLoading } = useTasks(workspaceId) From 9c9c1f856e730c00358091b98f5db3d3d6d02b06 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 24 Mar 2026 10:28:00 -0700 Subject: [PATCH 12/19] chore(tour): address remaining PR review comments - Remove unused logger import and instance in product-tour.tsx - Remove unused tour-tooltip-fade animation from tailwind config - Remove unnecessary overflow-hidden wrapper around WorkflowTour - Add border stroke to arrow SVG in tour-tooltip for visual consistency Co-Authored-By: Claude Opus 4.6 --- .../[workspaceId]/components/product-tour/product-tour.tsx | 3 --- .../app/workspace/[workspaceId]/w/[workflowId]/layout.tsx | 4 +--- .../components/emcn/components/tour-tooltip/tour-tooltip.tsx | 5 +++-- apps/sim/tailwind.config.ts | 5 ----- 4 files changed, 4 insertions(+), 13 deletions(-) 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 index 3a2ea168b7a..058ad62d7ca 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -1,7 +1,6 @@ 'use client' import { useMemo } from 'react' -import { createLogger } from '@sim/logger' import dynamic from 'next/dynamic' import { usePathname } from 'next/navigation' import { navTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps' @@ -12,8 +11,6 @@ import { } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' -const logger = createLogger('NavTour') - const Joyride = dynamic(() => import('react-joyride'), { ssr: false, }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx index d69fd3f9ed2..7da2877ecf0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx @@ -7,9 +7,7 @@ export default function WorkflowLayout({ children }: { children: React.ReactNode return (
{children} -
- -
+
) } diff --git a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx index 34f76fe99b9..e8acc41d21c 100644 --- a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx +++ b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx @@ -208,9 +208,10 @@ function TourTooltip({ height={7} viewBox='0 0 14 7' preserveAspectRatio='none' - className='-mt-px fill-[var(--bg)]' + className='-mt-px fill-[var(--bg)] stroke-[var(--border)]' > - + + diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts index 16e8d421ebd..fd6a8bf29d4 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -184,10 +184,6 @@ export default { from: { opacity: '0', transform: 'scale(0.96) translateY(4px)' }, to: { opacity: '1', transform: 'scale(1) translateY(0)' }, }, - 'tour-tooltip-fade': { - from: { opacity: '0' }, - to: { opacity: '1' }, - }, }, animation: { 'caret-blink': 'caret-blink 1.25s ease-out infinite', @@ -202,7 +198,6 @@ export default { '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)', - 'tour-tooltip-fade': 'tour-tooltip-fade 150ms cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, }, }, From 752d0438ab15b9bf79233272242cb52ad10a0eee Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 24 Mar 2026 10:47:59 -0700 Subject: [PATCH 13/19] chore(tour): address second round of PR review comments - Remove unnecessary 'use client' from workflow layout (children are already client components) - Fix ref guard timing issue in TourTooltipAdapter that could prevent Joyride from tracking tooltip on subsequent steps Co-Authored-By: Claude Opus 4.6 --- .../[workspaceId]/components/product-tour/tour-shared.tsx | 7 ++----- .../app/workspace/[workspaceId]/w/[workflowId]/layout.tsx | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) 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 index 44bbe5179cf..104c43d06ba 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx @@ -1,6 +1,6 @@ 'use client' -import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useState } from 'react' import type { TooltipRenderProps } from 'react-joyride' import { TourTooltip } from '@/components/emcn' @@ -60,10 +60,8 @@ export function TourTooltipAdapter({ }: TooltipRenderProps) { const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) const [targetEl, setTargetEl] = useState(null) - const hasSetRef = useRef(false) useEffect(() => { - hasSetRef.current = false const { target } = step if (typeof target === 'string') { setTargetEl(document.querySelector(target)) @@ -76,9 +74,8 @@ export function TourTooltipAdapter({ const refCallback = useCallback( (node: HTMLDivElement | null) => { - if (!hasSetRef.current && tooltipProps.ref) { + if (tooltipProps.ref) { ;(tooltipProps.ref as React.RefCallback)(node) - hasSetRef.current = true } }, [tooltipProps.ref] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx index 7da2877ecf0..28acbac079a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx @@ -1,5 +1,3 @@ -'use client' - import { WorkflowTour } from '@/app/workspace/[workspaceId]/components/product-tour' import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error' From d71f1a8ebaa28c21c53594c42bbe98d26ed14efc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 24 Mar 2026 11:21:29 -0700 Subject: [PATCH 14/19] chore(tour): extract shared Joyride config, fix popover arrow overflow - Extract duplicated Joyride floaterProps/styles into getSharedJoyrideProps() in tour-shared.tsx, parameterized by spotlightBorderRadius - Fix showArrow disabling content scrolling in PopoverContent by wrapping children in a scrollable div when arrow is visible Co-Authored-By: Claude Opus 4.6 --- .../components/product-tour/product-tour.tsx | 37 +-------------- .../components/product-tour/tour-shared.tsx | 46 +++++++++++++++++++ .../components/product-tour/workflow-tour.tsx | 37 +-------------- .../emcn/components/popover/popover.tsx | 6 ++- 4 files changed, 55 insertions(+), 71 deletions(-) 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 index 058ad62d7ca..8ff422038b4 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -8,6 +8,7 @@ import type { TourState } from '@/app/workspace/[workspaceId]/components/product import { TourStateContext, TourTooltipAdapter, + getSharedJoyrideProps, } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' @@ -55,41 +56,7 @@ export function NavTour() { disableOverlayClose spotlightPadding={4} tooltipComponent={TourTooltipAdapter} - 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: 8, - boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.55)', - position: 'fixed' as React.CSSProperties['position'], - 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)', - }, - 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'], - }, - }} + {...getSharedJoyrideProps({ spotlightBorderRadius: 8 })} /> ) 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 index 104c43d06ba..b0d7436ac4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx @@ -109,3 +109,49 @@ export function TourTooltipAdapter({ ) } + +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/workflow-tour.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx index 6bb55a8cb2a..7fdfa3e8127 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx @@ -6,6 +6,7 @@ import type { TourState } from '@/app/workspace/[workspaceId]/components/product import { TourStateContext, TourTooltipAdapter, + getSharedJoyrideProps, } 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' @@ -54,41 +55,7 @@ export function WorkflowTour() { disableOverlayClose spotlightPadding={1} tooltipComponent={TourTooltipAdapter} - 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: 6, - boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.55)', - position: 'fixed' as React.CSSProperties['position'], - 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)', - }, - 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'], - }, - }} + {...getSharedJoyrideProps({ spotlightBorderRadius: 6 })} /> ) diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index 6672fc5123b..bff818a9118 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -628,7 +628,11 @@ const PopoverContent = React.forwardRef< ...style, }} > - {children} + {showArrow ? ( +
{children}
+ ) : ( + children + )} {showArrow && ( Date: Tue, 24 Mar 2026 11:26:03 -0700 Subject: [PATCH 15/19] lint --- .../[workspaceId]/components/product-tour/product-tour.tsx | 2 +- .../[workspaceId]/components/product-tour/workflow-tour.tsx | 2 +- apps/sim/components/emcn/components/popover/popover.tsx | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) 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 index 8ff422038b4..1c49837afa5 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx @@ -6,9 +6,9 @@ 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, - getSharedJoyrideProps, } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' 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 index 7fdfa3e8127..13bcf7468c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx @@ -4,9 +4,9 @@ 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, - getSharedJoyrideProps, } 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' diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index bff818a9118..6e0c07622b2 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -628,11 +628,7 @@ const PopoverContent = React.forwardRef< ...style, }} > - {showArrow ? ( -
{children}
- ) : ( - children - )} + {showArrow ?
{children}
: children} {showArrow && ( Date: Tue, 24 Mar 2026 11:33:35 -0700 Subject: [PATCH 16/19] fix(tour): stop running tour when disabled becomes true Prevents nav and workflow tours from overlapping. When a user navigates to a workflow page while the nav tour is running, the disabled flag now stops the nav tour instead of just suppressing auto-start. Co-Authored-By: Claude Opus 4.6 --- .../[workspaceId]/components/product-tour/use-tour.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 index e57f5537b66..3cbc1d029e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts @@ -132,6 +132,16 @@ export function useTour({ [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 From c4bd242d27a830c415ba8de6d9784def37a92dbc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 24 Mar 2026 12:09:52 -0700 Subject: [PATCH 17/19] fix(tour): move auto-start flag into timer, fix truncate selector conflict - Move hasAutoStarted flag inside setTimeout callback so it's only set when the timer fires, allowing retry if disabled changes during delay - Add data-popover-scroll attribute to showArrow scroll wrapper and exclude it from the flex-1 truncate selector to prevent overflow conflict Co-Authored-By: Claude Opus 4.6 --- .../[workspaceId]/components/product-tour/use-tour.ts | 2 +- .../components/emcn/components/popover/popover.tsx | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) 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 index 3cbc1d029e2..03358b09655 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts @@ -145,9 +145,9 @@ export function useTour({ /** Auto-start on first visit */ useEffect(() => { if (disabled || hasAutoStarted.current) return - hasAutoStarted.current = true const timer = setTimeout(() => { + hasAutoStarted.current = true if (!isTourCompleted(storageKey)) { setStepIndex(0) setIsEntrance(true) diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index 6e0c07622b2..f837325d309 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -610,7 +610,8 @@ const PopoverContent = React.forwardRef< showArrow ? 'overflow-visible' : 'overflow-auto', STYLES.colorScheme[colorScheme].content, STYLES.content, - hasUserWidthConstraint && '[&_.flex-1]:truncate [&_[data-popover-section]]:truncate', + hasUserWidthConstraint && + '[&_.flex-1:not([data-popover-scroll])]:truncate [&_[data-popover-section]]:truncate', border && 'border border-[var(--border-1)]', className )} @@ -628,7 +629,13 @@ const PopoverContent = React.forwardRef< ...style, }} > - {showArrow ?
{children}
: children} + {showArrow ? ( +
+ {children} +
+ ) : ( + children + )} {showArrow && ( Date: Tue, 24 Mar 2026 12:24:49 -0700 Subject: [PATCH 18/19] fix(tour): remove duplicate overlay on center-placed tour steps Joyride's spotlight already renders a full-screen overlay via boxShadow. The centered TourTooltip was adding its own bg-black/55 overlay on top, causing double-darkened backgrounds. Removed the redundant overlay div. Co-Authored-By: Claude Opus 4.6 --- .../sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx index e8acc41d21c..d4d62dade62 100644 --- a/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx +++ b/apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx @@ -169,7 +169,6 @@ function TourTooltip({ if (isCentered) { return createPortal(
-
Date: Tue, 24 Mar 2026 12:30:21 -0700 Subject: [PATCH 19/19] refactor: move docs link from settings to help dropdown The Docs link (https://docs.sim.ai) was buried in settings navigation. Moved it to the Help dropdown in the sidebar for better discoverability. Co-Authored-By: Claude Opus 4.6 --- .../app/workspace/[workspaceId]/settings/navigation.ts | 10 ---------- .../[workspaceId]/w/components/sidebar/sidebar.tsx | 9 +++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index 9ec049a209b..d4ebc9f75e1 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/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index b8790653c0b..7be6c8b4534 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -22,6 +22,7 @@ import { Tooltip, } from '@/components/emcn' import { + BookOpen, Calendar, Database, File, @@ -1418,6 +1419,14 @@ export const Sidebar = memo(function Sidebar() { )} + + window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer') + } + > + + Docs + setIsHelpModalOpen(true)}> Report an issue