From 86cf416692d37a307f4a32f3d640bc673ed35c39 Mon Sep 17 00:00:00 2001 From: Krishna Mohan Date: Fri, 27 Feb 2026 15:27:22 +0530 Subject: [PATCH 01/10] feat: Add first-time user onboarding tutorial with spotlight-based guided walkthrough Implement a comprehensive onboarding experience for first-time users featuring: - Spotlight-based guided tour that highlights actual UI elements in the sidebar - 6-step walkthrough covering Workflow Builder, Templates, Schedules, Action Center, and Manage section - Smooth spotlight transitions and pulsing rings for visual emphasis - Tooltip cards positioned next to highlighted elements - Sidebar forced open during onboarding for visibility - Automatic Manage section expansion on the final step - Keyboard navigation (arrow keys and Escape support) - First-time detection via localStorage persistence - Auth-gated (only shows for authenticated users) - Zero new dependencies (uses existing Dialog, Button, Lucide icons, Tailwind) Architecture: - useOnboardingStore (Zustand): Manages onboarding state with localStorage persistence - OnboardingDialog component: Spotlight tour with createPortal rendering for proper z-index - AppLayout integration: Data attributes on sidebar items, sidebar forcing, settings auto-expand Signed-off-by: Krishna Mohan --- frontend/src/components/layout/AppLayout.tsx | 39 +- .../onboarding/OnboardingDialog.tsx | 343 ++++++++++++++++++ frontend/src/store/onboardingStore.ts | 30 ++ 3 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/onboarding/OnboardingDialog.tsx create mode 100644 frontend/src/store/onboardingStore.ts diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index 25bd9360..f66309d6 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -41,6 +41,8 @@ import { setMobilePlacementSidebarClose } from '@/components/layout/sidebar-stat import { useCommandPaletteStore } from '@/store/commandPaletteStore'; import { usePrefetchOnIdle } from '@/hooks/usePrefetchOnIdle'; import { prefetchIdleRoutes, prefetchRoute } from '@/lib/prefetch-routes'; +import { OnboardingDialog } from '@/components/onboarding/OnboardingDialog'; +import { useOnboardingStore } from '@/store/onboardingStore'; interface AppLayoutProps { children: React.ReactNode; @@ -88,6 +90,12 @@ export function AppLayout({ children }: AppLayoutProps) { const { theme, startTransition } = useThemeStore(); const openCommandPalette = useCommandPaletteStore((state) => state.open); + // Onboarding tutorial state + const hasCompletedOnboarding = useOnboardingStore((state) => state.hasCompletedOnboarding); + const onboardingStep = useOnboardingStore((state) => state.currentStep); + const setOnboardingStep = useOnboardingStore((state) => state.setCurrentStep); + const completeOnboarding = useOnboardingStore((state) => state.completeOnboarding); + // Get git SHA for version display (monorepo - same for frontend and backend) const gitSha = env.VITE_GIT_SHA; // If it's a tag (starts with v), show full tag. Otherwise show first 7 chars of SHA @@ -100,7 +108,13 @@ export function AppLayout({ children }: AppLayoutProps) { // Auto-collapse sidebar when opening workflow builder, expand for other routes // On mobile, always start collapsed + // During onboarding, force sidebar open so highlighted elements are visible useEffect(() => { + if (isAuthenticated && !hasCompletedOnboarding) { + setSidebarOpen(true); + setWasExplicitlyOpened(true); + return; + } if (isMobile) { setSidebarOpen(false); setWasExplicitlyOpened(false); @@ -112,7 +126,14 @@ export function AppLayout({ children }: AppLayoutProps) { setSidebarOpen(!isWorkflowRoute); setWasExplicitlyOpened(!isWorkflowRoute); } - }, [location.pathname, isMobile]); + }, [location.pathname, isMobile, isAuthenticated, hasCompletedOnboarding]); + + // Expand Manage section when onboarding highlights it + useEffect(() => { + if (isAuthenticated && !hasCompletedOnboarding && onboardingStep === 5) { + setSettingsOpen(true); + } + }, [isAuthenticated, hasCompletedOnboarding, onboardingStep]); // Close sidebar on mobile when navigating useEffect(() => { @@ -268,6 +289,14 @@ export function AppLayout({ children }: AppLayoutProps) { toggle: handleToggle, }; + // Onboarding target IDs for spotlight tour + const onboardingTargetIds: Record = { + '/': 'workflow-builder', + '/templates': 'template-library', + '/schedules': 'schedules', + '/action-center': 'action-center', + }; + const navigationItems = [ { name: 'Workflow Builder', @@ -382,6 +411,12 @@ export function AppLayout({ children }: AppLayoutProps) { return ( +
{/* Mobile backdrop overlay */} {isMobile && sidebarOpen && ( @@ -493,6 +528,7 @@ export function AppLayout({ children }: AppLayoutProps) { prefetchRoute(item.href)} onClick={(e) => { // If modifier key is held (CMD+click, Ctrl+click), link opens in new tab @@ -541,6 +577,7 @@ export function AppLayout({ children }: AppLayoutProps) { {/* Manage Collapsible Section */}
+ + {/* Step content */} +
+
+
+ +
+
+

{step.title}

+

{step.description}

+
+
+ +

{step.content}

+
+ + {/* Navigation footer */} +
+ + +
+ {currentStep > 0 && ( + + )} + +
+
+ + {/* Step counter */} +
+ + {currentStep + 1} of {ONBOARDING_STEPS.length} + +
+
+ , + document.body, + ); +} diff --git a/frontend/src/store/onboardingStore.ts b/frontend/src/store/onboardingStore.ts new file mode 100644 index 00000000..f1827dbc --- /dev/null +++ b/frontend/src/store/onboardingStore.ts @@ -0,0 +1,30 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface OnboardingState { + hasCompletedOnboarding: boolean; + currentStep: number; + setCurrentStep: (step: number) => void; + nextStep: () => void; + prevStep: () => void; + completeOnboarding: () => void; + resetOnboarding: () => void; +} + +export const useOnboardingStore = create()( + persist( + (set, get) => ({ + hasCompletedOnboarding: false, + currentStep: 0, + setCurrentStep: (step) => set({ currentStep: step }), + nextStep: () => set({ currentStep: Math.min(5, get().currentStep + 1) }), + prevStep: () => set({ currentStep: Math.max(0, get().currentStep - 1) }), + completeOnboarding: () => set({ hasCompletedOnboarding: true, currentStep: 0 }), + resetOnboarding: () => set({ hasCompletedOnboarding: false, currentStep: 0 }), + }), + { + name: 'shipsec-onboarding', + partialize: (state) => ({ hasCompletedOnboarding: state.hasCompletedOnboarding }), // Only persist hasCompletedOnboarding, not currentStep + }, + ), +); From cacfe5b840e4d1791dac2b60aa8ca5bd622ff912 Mon Sep 17 00:00:00 2001 From: Krishna Mohan Date: Mon, 2 Mar 2026 13:30:21 +0530 Subject: [PATCH 02/10] feat: Add spotlight-based guided tour for Workflow Builder Extends the onboarding system with a dedicated Workflow Builder tour that walks first-time users through the builder interface: component library, canvas, design/execute modes, save, run, and more options. Signed-off-by: Krishna Mohan --- frontend/src/components/layout/TopBar.tsx | 15 +- .../onboarding/WorkflowBuilderTour.tsx | 364 ++++++++++++++++++ .../workflow/WorkflowBuilderShell.tsx | 5 +- .../workflow-builder/WorkflowBuilder.tsx | 14 + frontend/src/store/onboardingStore.ts | 19 +- 5 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/onboarding/WorkflowBuilderTour.tsx diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index bc994475..e05ac9ea 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -194,7 +194,10 @@ export function TopBar({ }, [metadata.name]); const modeToggle = ( -
+
diff --git a/frontend/src/components/onboarding/WorkflowBuilderTour.tsx b/frontend/src/components/onboarding/WorkflowBuilderTour.tsx new file mode 100644 index 00000000..f95e5c8a --- /dev/null +++ b/frontend/src/components/onboarding/WorkflowBuilderTour.tsx @@ -0,0 +1,364 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { Button } from '@/components/ui/button'; +import { + Sparkles, + PanelLeft, + MousePointerSquareDashed, + PencilLine, + Save, + Play, + MoreVertical, + ArrowRight, + ArrowLeft, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface BuilderTourStep { + title: string; + description: string; + icon: typeof Sparkles; + content: string; + color: string; + target: string | null; +} + +const BUILDER_TOUR_STEPS: BuilderTourStep[] = [ + { + title: 'Welcome to the Workflow Builder!', + description: "Let's explore how to build workflows.", + icon: Sparkles, + content: + "This is where you design and execute security workflows. We'll walk you through each part of the builder interface.", + color: 'text-purple-500', + target: null, + }, + { + title: 'Component Library', + description: 'Drag and drop nodes onto the canvas.', + icon: PanelLeft, + content: + 'Browse available components here — entry points, actions, conditions, and more. Drag any component onto the canvas to add it to your workflow.', + color: 'text-blue-500', + target: '[data-onboarding-builder="library-panel"]', + }, + { + title: 'Canvas', + description: 'Your workflow design surface.', + icon: MousePointerSquareDashed, + content: + 'This is where your workflow comes to life. Connect nodes by dragging from one handle to another. Pan and zoom to navigate larger workflows.', + color: 'text-emerald-500', + target: '[data-onboarding-builder="canvas"]', + }, + { + title: 'Design & Execute Modes', + description: 'Switch between building and running.', + icon: PencilLine, + content: + 'Use Design mode to build your workflow. Switch to Execute mode to run it, inspect past executions, and view real-time logs.', + color: 'text-indigo-500', + target: '[data-onboarding-builder="mode-toggle"]', + }, + { + title: 'Save Workflow', + description: 'Persist your changes.', + icon: Save, + content: + 'Save your workflow at any time. The status badge shows whether changes are synced, pending, or currently saving. Use Cmd+S for a quick save.', + color: 'text-amber-500', + target: '[data-onboarding-builder="save-button"]', + }, + { + title: 'Run Workflow', + description: 'Execute your workflow instantly.', + icon: Play, + content: + 'Click Run to execute your workflow. If your workflow has runtime inputs, a dialog will prompt you to fill them in before execution starts.', + color: 'text-green-500', + target: '[data-onboarding-builder="run-button"]', + }, + { + title: 'More Options', + description: 'Undo, Redo, Import & Export.', + icon: MoreVertical, + content: + 'Access undo/redo (Cmd+Z / Cmd+Shift+Z), import workflows from JSON files, or export your current workflow for sharing and backup.', + color: 'text-red-500', + target: '[data-onboarding-builder="more-options"]', + }, +]; + +const SPOTLIGHT_PADDING = 8; +const TOOLTIP_GAP = 16; +const TOOLTIP_WIDTH = 360; + +interface WorkflowBuilderTourProps { + open: boolean; + onComplete: () => void; + currentStep: number; + onStepChange: (step: number) => void; +} + +export function WorkflowBuilderTour({ + open, + onComplete, + currentStep, + onStepChange, +}: WorkflowBuilderTourProps) { + const [targetRect, setTargetRect] = useState(null); + const tooltipRef = useRef(null); + + const step = BUILDER_TOUR_STEPS[currentStep]; + const isLastStep = currentStep === BUILDER_TOUR_STEPS.length - 1; + const Icon = step?.icon ?? Sparkles; + const isCenter = !step?.target; + + // Track target element position + useEffect(() => { + if (!open) { + setTargetRect(null); + return; + } + + if (!step?.target) { + setTargetRect(null); + return; + } + + // Delay to allow panel animations to settle + const timer = setTimeout(() => { + const el = document.querySelector(step.target!); + if (el) { + setTargetRect(el.getBoundingClientRect()); + } else { + setTargetRect(null); + } + }, 350); + + return () => clearTimeout(timer); + }, [open, step?.target, currentStep]); + + // Update on window resize + useEffect(() => { + if (!open || !step?.target) return; + + const updateRect = () => { + const el = document.querySelector(step.target!); + if (el) { + setTargetRect(el.getBoundingClientRect()); + } + }; + + window.addEventListener('resize', updateRect); + return () => window.removeEventListener('resize', updateRect); + }, [open, step?.target]); + + const handleNext = useCallback(() => { + if (isLastStep) { + onComplete(); + } else { + onStepChange(currentStep + 1); + } + }, [isLastStep, onComplete, onStepChange, currentStep]); + + const handleBack = useCallback(() => { + if (currentStep > 0) { + onStepChange(currentStep - 1); + } + }, [currentStep, onStepChange]); + + // Global keyboard handler + useEffect(() => { + if (!open) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowRight') { + e.preventDefault(); + handleNext(); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + handleBack(); + } else if (e.key === 'Escape') { + e.preventDefault(); + onComplete(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [open, handleNext, handleBack, onComplete]); + + if (!open || !step) return null; + + // Calculate tooltip position + const getTooltipStyle = (): React.CSSProperties => { + if (isCenter || !targetRect) { + return { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: TOOLTIP_WIDTH, + }; + } + + // For the canvas (large element), position in the center of it + if (step.target === '[data-onboarding-builder="canvas"]') { + return { + top: targetRect.top + targetRect.height / 2 - 100, + left: targetRect.left + targetRect.width / 2 - TOOLTIP_WIDTH / 2, + width: TOOLTIP_WIDTH, + }; + } + + // For the library panel, position to the right + if (step.target === '[data-onboarding-builder="library-panel"]') { + const tooltipTop = Math.max(80, targetRect.top + targetRect.height / 2 - 100); + return { + top: tooltipTop, + left: targetRect.right + TOOLTIP_GAP, + width: TOOLTIP_WIDTH, + }; + } + + // For top-bar elements, position below + const tooltipLeft = Math.max( + 16, + Math.min( + window.innerWidth - TOOLTIP_WIDTH - 16, + targetRect.left + targetRect.width / 2 - TOOLTIP_WIDTH / 2, + ), + ); + + return { + top: targetRect.bottom + TOOLTIP_GAP, + left: tooltipLeft, + width: TOOLTIP_WIDTH, + }; + }; + + return createPortal( +
+ {/* Click blocker overlay */} +