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 6512d7212f..47deb43b58 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 0000000000..7713bf725f --- /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 0000000000..2109695be6 --- /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 0000000000..91a683e841 --- /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 0000000000..b14a63dcfd --- /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 f681db98c6..1f5c5b785a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -322,11 +322,14 @@ export function Home({ chatId }: HomeProps = {}) { return (
-

+

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

-
+
diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index 45a92298f3..da884b9257 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 f115a655f3..83f8e0ae20 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 47deb43b58..6512d7212f 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 2109695be6..c25d71c16f 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 b14a63dcfd..e5c52ac9ee 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 07bd5a3892..aaf4146f03 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 8702c41a5a..f73be6400a 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 a5cb116132..c588585b3d 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 f12f246837..fd6a8bf29d 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -180,6 +180,10 @@ export default { from: { opacity: '0', transform: 'translateY(20px)' }, to: { opacity: '1', transform: 'translateY(0)' }, }, + 'tour-tooltip-in': { + from: { opacity: '0', transform: 'scale(0.96) translateY(4px)' }, + to: { opacity: '1', transform: 'scale(1) translateY(0)' }, + }, }, animation: { 'caret-blink': 'caret-blink 1.25s ease-out infinite', @@ -193,6 +197,7 @@ export default { 'thinking-block': 'thinking-block 1.6s ease-in-out infinite', 'slide-in-right': 'slide-in-right 350ms ease-out forwards', 'slide-in-bottom': 'slide-in-bottom 400ms cubic-bezier(0.16, 1, 0.3, 1)', + 'tour-tooltip-in': 'tour-tooltip-in 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, }, }, 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 7713bf725f..d1aca2af4e 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 0000000000..02f3743f4c --- /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 c25d71c16f..41504e5554 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 91a683e841..0000000000 --- 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 e5c52ac9ee..0000000000 --- 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 0000000000..bef04c4a90 --- /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 0000000000..dbcfade981 --- /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 0000000000..9d3006ea73 --- /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 da884b9257..dba9198ba5 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 83f8e0ae20..6fe1eb3b9a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx @@ -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 aaf4146f03..ebb6a6ecab 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 1f19d7d4b7..30b4aa349c 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 0000000000..3a3de2ac5b --- /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 fd6a8bf29d..16e8d421eb 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 02f3743f4c..0ce4af4737 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 41504e5554..f9f6cd2416 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 bef04c4a90..13fc7ffab3 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 dbcfade981..4a42555869 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 9d3006ea73..b0964f1c6a 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 7da2877ecf..d69fd3f9ed 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 30b4aa349c..2a93e4ade9 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 3a3de2ac5b..b35f09e669 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 ebb6a6ecab..521990d0b4 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 95af05f41c..4a7bc682f6 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 b35f09e669..42a693a541 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 f73be6400a..6672fc5123 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 f9f6cd2416..680cd976ff 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 13fc7ffab3..e57f5537b6 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 d1aca2af4e..38779f28cb 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 0ce4af4737..632d94357d 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 680cd976ff..bc9d3594cc 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 b0964f1c6a..e782791a44 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 d98465cbb6..44942c64c2 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 521990d0b4..2e3cf0d649 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 42a693a541..34f76fe99b 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 bc9d3594cc..3a2ea168b7 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 0000000000..44bbe5179c --- /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 e782791a44..6bb55a8cb2 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 2e3cf0d649..b8790653c0 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 3a2ea168b7..058ad62d7c 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 d69fd3f9ed..7da2877ecf 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 34f76fe99b..e8acc41d21 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 16e8d421eb..fd6a8bf29d 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 44bbe5179c..104c43d06b 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 7da2877ecf..28acbac079 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 058ad62d7c..8ff422038b 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 104c43d06b..b0d7436ac4 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 6bb55a8cb2..7fdfa3e812 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 6672fc5123..bff818a911 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 8ff422038b..1c49837afa 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 7fdfa3e812..13bcf7468c 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 bff818a911..6e0c07622b 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 e57f5537b6..3cbc1d029e 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 3cbc1d029e..03358b0965 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 6e0c07622b..f837325d30 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 e8acc41d21..d4d62dade6 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 9ec049a209..d4ebc9f75e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -1,5 +1,4 @@ import { - BookOpen, Card, Connections, HexSimple, @@ -38,7 +37,6 @@ export type SettingsSection = | 'skills' | 'workflow-mcp-servers' | 'inbox' - | 'docs' | 'admin' | 'recently-deleted' @@ -156,14 +154,6 @@ export const allNavigationItems: NavigationItem[] = [ requiresEnterprise: true, selfHostedOverride: isSSOEnabled, }, - { - id: 'docs', - label: 'Docs', - icon: BookOpen, - section: 'system', - requiresHosted: true, - externalUrl: 'https://docs.sim.ai', - }, { id: 'admin', label: 'Admin', diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index b8790653c0..7be6c8b453 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