diff --git a/apps/apollo-vertex/app/patterns/_meta.ts b/apps/apollo-vertex/app/patterns/_meta.ts index 3e0062d3d..e3a799d88 100644 --- a/apps/apollo-vertex/app/patterns/_meta.ts +++ b/apps/apollo-vertex/app/patterns/_meta.ts @@ -2,6 +2,7 @@ export default { "ai-chat": "AI Chat", "feedback-vote-widget": "Feedback Vote Widget", "metric-card": "Metric Card", + "onboarding-tour-joyride": "Onboarding tour (Joyride)", "page-header": "Page Header", shell: "Shell", }; diff --git a/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/onboarding-tour-joyride-demo.tsx b/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/onboarding-tour-joyride-demo.tsx new file mode 100644 index 000000000..cf50a238c --- /dev/null +++ b/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/onboarding-tour-joyride-demo.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { Button } from "@/registry/button/button"; +import type { TourDefinition } from "@/registry/onboarding-tour-joyride"; +import { + OnboardingTourProvider, + resetTourState, + useOnboardingTour, +} from "@/registry/onboarding-tour-joyride"; + +const DEMO_TOUR: TourDefinition = { + id: "onboarding-tour-joyride-page-demo", + steps: [ + { + id: "welcome", + type: "modal", + title: "A tour of the tour component", + body: "This is the onboarding tour running on this very page. Click through to see how each piece looks in context.", + image: "/assets/onboarding-tour-welcome.png", + nextLabel: "Show me around", + }, + { + id: "welcome-modal-preview", + selector: "#tour-welcome-preview", + title: "Welcome modals", + body: "Modal-type steps render centered over your app with an image, headline, body, and CTA — ideal for intros and success screens.", + placement: "top", + }, + { + id: "step-popover-preview", + selector: "#tour-popover-preview", + title: "Step popovers", + body: "The default step type is a popover anchored to any element — progress bars, a tip slot, and navigation.", + tip: "You're looking at one right now.", + placement: "top", + }, + { + id: "features", + selector: "#tour-features", + title: "What's included", + body: "Spotlight overlay, keyboard navigation, conditional steps, persistence, and dark mode support.", + placement: "top", + }, + { + id: "install", + selector: "#tour-install", + title: "Drop it in", + body: "One command to add it to your project.", + placement: "top", + }, + ], +}; + +const DEMO_TOURS = [DEMO_TOUR]; + +function TourTrigger() { + const { startTour } = useOnboardingTour(); + + return ( + + ); +} + +export function OnboardingTourJoyrideDemoTrigger() { + return ( + + + + ); +} diff --git a/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/onboarding-tour-joyride-static-previews.tsx b/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/onboarding-tour-joyride-static-previews.tsx new file mode 100644 index 000000000..757478d37 --- /dev/null +++ b/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/onboarding-tour-joyride-static-previews.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { OnboardingTourJoyridePopoverCard } from "@/registry/onboarding-tour-joyride/onboarding-tour-joyride-popover"; +import { OnboardingTourJoyrideWelcomeModalCard } from "@/registry/onboarding-tour-joyride/onboarding-tour-joyride-welcome-modal"; + +function noop() { + // no-op for static previews +} + +export function WelcomeModalPreview() { + return ( +
+
+ +
+
+ ); +} + +export function PopoverStepPreview() { + return ( +
+
+ +
+
+ ); +} diff --git a/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/page.mdx b/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/page.mdx new file mode 100644 index 000000000..d0a41b273 --- /dev/null +++ b/apps/apollo-vertex/app/patterns/onboarding-tour-joyride/page.mdx @@ -0,0 +1,214 @@ +import { WelcomeModalPreview, PopoverStepPreview } from './onboarding-tour-joyride-static-previews'; +import { OnboardingTourJoyrideDemoTrigger } from './onboarding-tour-joyride-demo'; + +# Onboarding tour (Joyride) + +A guided onboarding tour system powered by [React Joyride](https://docs.react-joyride.com/) with a custom Apollo Vertex–styled tooltip, welcome modals, and step-by-step navigation. Consumers define their own tour steps and point them at app-specific elements via CSS selectors. + +
+ +
+ +## Welcome modal + +Use the `type: 'modal'` step to show a full-screen centered modal — ideal for welcome screens, success states, or intro steps. + +
+ +
+ +## Step popover + +The default step type renders a card-style popover with progress bars, navigation, and an optional tip section. + +
+ +
+ +## Features + +
+ +- **Spotlight overlay**: Highlights target elements with a cutout in a dimmed overlay (via React Joyride) +- **Popover cards**: Step-by-step cards with progress bars, title, body, tips, and navigation +- **Welcome modal**: Full-screen modal variant for intro/success steps with optional images +- **CSS selector targeting**: Target elements by CSS selector — no wrapper components needed +- **Smooth transitions**: Native React rendering of the tooltip animates cleanly between steps +- **Conditional steps**: Gate steps on conditions with type-safe generics +- **Keyboard navigation**: Escape to skip the tour, focus trap via React Joyride +- **Persistence**: Tracks completed tours in localStorage so they don't repeat +- **Dark mode**: Automatically adapts overlay color to the current theme + +
+ +## Installation + +
+ +```bash +npx shadcn@latest add @uipath/onboarding-tour-joyride +``` + +
+ +## Usage + +### 1. Define your tour + +```tsx +import type { TourDefinition } from "@/components/ui/onboarding-tour-joyride"; + +const myTour: TourDefinition = { + id: "my-onboarding-tour", + steps: [ + { + id: "welcome", + type: "modal", + title: "Welcome!", + body: "Let us show you around your new workspace.", + tip: "This tour will only show once.", + nextLabel: "Let's go", + image: "/assets/welcome.png", // optional + }, + { + id: "sidebar", + selector: "[data-sidebar]", + title: "Navigation", + body: "Use the sidebar to move between sections.", + placement: "right", + }, + { + id: "search", + selector: "#search-bar", + title: "Quick search", + body: "Find anything across your workspace.", + placement: "bottom", + tip: "Try pressing Cmd+K for a shortcut.", + }, + ], +}; +``` + +### 2. Set up the provider + +Wrap your app (or a section of it) with `OnboardingTourProvider`: + +```tsx +import { + OnboardingTourProvider, +} from "@/components/ui/onboarding-tour-joyride"; + +function App() { + return ( + + + + ); +} +``` + +### 3. Start the tour + +```tsx +import { useOnboardingTour, isTourCompleted } from "@/components/ui/onboarding-tour-joyride"; + +function WelcomeBanner() { + const { startTour } = useOnboardingTour(); + + if (isTourCompleted("my-onboarding-tour")) return null; + + return ( + + ); +} +``` + +## Type-safe conditional steps + +Use generics to make condition keys type-safe. The `waitFor` property gates steps on conditions set via `setCondition`: + +```tsx +type MyConditions = "profile-saved" | "data-loaded"; + +const tour: TourDefinition = { + id: "conditional-tour", + steps: [ + { + id: "intro", + selector: "#intro-section", + title: "Getting started", + body: "First, let's set up your profile.", + placement: "right", + }, + { + id: "results", + selector: "#results-panel", + title: "Your results", + body: "Here's what we found.", + placement: "left", + waitFor: "profile-saved", // type-safe — must be a MyConditions key + }, + ], +}; + +// In your component: +const { setCondition } = useOnboardingTour(); + +async function handleSaveProfile(formData: FormData) { + await saveProfile(formData); + setCondition("profile-saved", true); // type-safe +} +``` + +## Resetting a tour + +Import `resetTourState` to clear completion state so a tour can be shown again: + +```tsx +import { resetTourState } from "@/components/ui/onboarding-tour-joyride"; + +function SettingsPage() { + return ( + + ); +} +``` + +## Step types + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `string` | Unique step identifier | +| `selector` | `string` | CSS selector for the target element. Omit for centered/modal steps. | +| `title` | `string` | Step title | +| `body` | `ReactNode` | Step body content | +| `tip` | `string` | Optional tip shown with a lightbulb icon | +| `placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | Popover position relative to target | +| `type` | `'popover' \| 'modal'` | Render as popover card (default) or welcome modal | +| `image` | `string` | Image URL for modal-type steps | +| `waitFor` | `TCondition` | Type-safe condition key that must be true before step can be reached | +| `nextLabel` | `string` | Custom label for the Next button | +| `onEnter` | `() => void` | Callback when step becomes active | + +## Components + +| Component | Description | +|-----------|-------------| +| `OnboardingTourProvider` | Context provider — wrap your app with this. Accepts `tours` and optional `initialConditions`. | +| `OnboardingTourJoyridePopover` | Joyride `tooltipComponent` that renders the step card. | +| `OnboardingTourJoyridePopoverCard` | Standalone popover card (useful for previews or custom renderers). | +| `OnboardingTourJoyrideWelcomeModal` | The welcome/success modal (portal + focus trap via Radix Dialog). | +| `OnboardingTourJoyrideWelcomeModalCard` | Standalone modal card (useful for previews). | + +## API + +| Export | Description | +|--------|-------------| +| `useOnboardingTour()` | Access tour actions: `startTour`, `setCondition`. Must be inside `OnboardingTourProvider`. | +| `isTourCompleted(tourId)` | Check whether a tour has been completed. | +| `markTourCompleted(tourId)` | Manually mark a tour as completed. | +| `resetTourState(tourId)` | Clear completion state so the tour can be shown again. | diff --git a/apps/apollo-vertex/public/assets/onboarding-tour-welcome.png b/apps/apollo-vertex/public/assets/onboarding-tour-welcome.png new file mode 100644 index 000000000..162a4dd7a Binary files /dev/null and b/apps/apollo-vertex/public/assets/onboarding-tour-welcome.png differ