Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/apollo-vertex/app/patterns/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
Original file line number Diff line number Diff line change
@@ -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 (
<Button
onClick={() => {
resetTourState(DEMO_TOUR.id);
startTour(DEMO_TOUR.id);
}}
>
{"Start tour demo"}
</Button>
);
}

export function OnboardingTourJoyrideDemoTrigger() {
return (
<OnboardingTourProvider tours={DEMO_TOURS}>
<TourTrigger />
</OnboardingTourProvider>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center justify-center p-8 bg-muted/20 rounded-lg">
<div id="tour-welcome-preview">
<OnboardingTourJoyrideWelcomeModalCard
title="Welcome to your workspace!"
body="Your first summary template is ready."
description="We've created a starting point based on your use case. Review the summary, refine sections as needed, and preview again until it fits your workflow."
image="/assets/onboarding-tour-welcome.png"
nextLabel="Let's go"
onNext={noop}
onClose={noop}
/>
</div>
</div>
);
}

export function PopoverStepPreview() {
return (
<div className="flex items-center justify-center p-8 bg-muted/20 rounded-lg">
<div id="tour-popover-preview">
<OnboardingTourJoyridePopoverCard
title="Preview with a real record"
body="Use this record to check how your instructions apply to real medical data. You can switch to your own records at any time from the dropdown."
tip="Try uploading your own documents for a more accurate preview."
currentStep={2}
totalSteps={5}
showBack
onBack={noop}
onNext={noop}
onSkip={noop}
/>
</div>
</div>
);
}
214 changes: 214 additions & 0 deletions apps/apollo-vertex/app/patterns/onboarding-tour-joyride/page.mdx
Original file line number Diff line number Diff line change
@@ -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.

<div className="not-prose my-6">
<OnboardingTourJoyrideDemoTrigger />
</div>

## Welcome modal

Use the `type: 'modal'` step to show a full-screen centered modal — ideal for welcome screens, success states, or intro steps.

<div className="not-prose my-8">
<WelcomeModalPreview />
</div>

## Step popover

The default step type renders a card-style popover with progress bars, navigation, and an optional tip section.

<div className="not-prose my-8">
<PopoverStepPreview />
</div>

## Features

<div id="tour-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

</div>

## Installation

<div id="tour-install">

```bash
npx shadcn@latest add @uipath/onboarding-tour-joyride
```

</div>

## 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 (
<OnboardingTourProvider tours={[myTour]}>
<YourApp />
</OnboardingTourProvider>
);
}
```

### 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 (
<button onClick={() => startTour("my-onboarding-tour")}>
Take a tour
</button>
);
}
```

## 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<MyConditions> = {
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<MyConditions>();

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 (
<button onClick={() => resetTourState("my-onboarding-tour")}>
Replay onboarding tour
</button>
);
}
```

## 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<TCondition>()` | 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. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading