Shepherd.js Tour Integration Guide — FlowPOS PWA
Complete guide for adding in-app guided onboarding tours to React forms using Shepherd.js within the FlowPOS frontend-pwa.
Table of Contents
- Architecture Overview
- System Components
- Tour Definition Schema
- Step-by-Step: Adding a New Tour
- Handling Tabs, Modals & Conditional UI
- DOM Anchoring with
data-tour - Shepherd Runner Internals
- i18n Integration
- Persistence & State Management
- Role-Based Filtering
- Styling & Theming
- Troubleshooting
- Testing Checklist
- Prompt Pack for AI-Assisted Development
1. Architecture Overview
The tour system follows a registry → hook → runner pattern:
tourRegistry (definitions)
↓
useTour (hook — state, filtering, lifecycle)
↓
shepherdRunner (execution — Shepherd.js instance, tab switching, DOM waiting)
↓
TourLauncher (UI — trigger button, tour list, reset)
Key design decisions:
- Shepherd.js is used for tours that span multiple tabs or require programmatic DOM preparation (e.g. the sales workflow tour). It provides
beforeShowPromisewhich is critical for async tab switching. - Driver.js is used for simpler tours where all targets exist in the DOM simultaneously (e.g. the main navigation tour).
- localStorage persists completion state per user+business pair — no backend storage needed.
- Custom DOM events bridge the gap between the tour runner (vanilla JS) and React state (tab switching).
File Map
| File | Purpose |
|---|---|
src/tours/types.ts | TypeScript types for tour definitions and state |
src/tours/index.ts | Tour registry — maps TourId → TourDefinition |
src/tours/shepherdRunner.ts | Shepherd.js tour executor with tab switching logic |
src/tours/mainTour.ts | Main onboarding tour definition |
src/tours/salesTour.ts | Sales workflow tour definition |
src/tours/tours.css | Theme overrides for both Driver.js and Shepherd.js |
src/hooks/useTour.ts | React hook — state management, auto-launch, start/reset |
src/components/TourLauncher.tsx | UI component — help button + tour menu dropdown |
src/lib/constants.ts | Storage keys and custom event names |
2. System Components
2.1 Tour Registry (src/tours/index.ts)
Central map of all tour definitions, keyed by TourId:
import { mainTour } from "./mainTour";
import { salesTour } from "./salesTour";
import type { TourDefinition, TourId } from "./types";
export const tourRegistry: Record<TourId, TourDefinition> = {
"main-onboarding": mainTour,
"sales-workflow": salesTour,
};
When you add a new tour, you register it here and extend the TourId union type.
2.2 useTour Hook (src/hooks/useTour.ts)
The hook is the central API for tour consumers:
const {
startTour, // (tourId: TourId) => void — launches a tour
checkAndAutoLaunch, // () => boolean — checks for pending auto-launch tours
resetTour, // (tourId: TourId) => void — clears completion state
isTourCompleted, // (tourId: TourId) => boolean
tourStates, // TourStateStore — all tour states
availableTours, // TourDefinition[] — filtered by current user role
} = useTour();
Routing logic inside startTour:
- Looks up the
TourDefinitionfrom the registry. - Filters steps by the current user's role.
- For Shepherd tours: calls
startShepherdSalesWorkflowTour()with callbacks. - For Driver.js tours: optionally filters by DOM presence, then calls
driver().drive().
2.3 TourLauncher (src/components/TourLauncher.tsx)
A help-icon button (CircleHelp) in the app header that opens a dropdown listing available tours. Each tour shows:
- Name and description (translated)
- Completion status (green "Completed" or gray "Not completed")
- "Start Tour" button
- "Reset" button (only when completed)
Before starting, the launcher checks if navigation is needed (requiredPath, requiredMainSelectedForm) and navigates with a 500ms delay to allow React to mount the target route.
2.4 Shepherd Runner (src/tours/shepherdRunner.ts)
The runner is responsible for:
- Creating a
Shepherd.Tourinstance with modal overlay - Adding steps with
beforeShowPromisehooks for async DOM preparation - Mapping step targets to required tabs via
STEP_TAB_MAP - Dispatching
CustomEventto switch React tabs - Waiting for the DOM element to appear (with retry logic)
- Wiring up Prev/Next/Done buttons with i18n labels
- Calling
onDone/onCancelcallbacks for state persistence
2.5 AuthenticatedLayout (Auto-Launch)
The layout component calls checkAndAutoLaunch() on mount with a two-phase retry:
Mount → wait 500ms → checkAndAutoLaunch()
├─ launched? → done
└─ not launched? → wait 1000ms more → checkAndAutoLaunch() (retry)
This handles cases where DOM elements haven't rendered yet on first check.
3. Tour Definition Schema
TourDefinition
interface TourDefinition {
id: TourId; // Unique identifier (union type)
nameKey: string; // i18n key for tour name
descriptionKey: string; // i18n key for tour description
roles: UniqueRoleName[]; // Which roles can see this tour
autoLaunch: boolean; // Auto-start on first visit?
steps: TourStepDefinition[]; // Ordered list of steps
requiredPath?: string; // Route the user must be on (e.g. "/main")
requiredMainSelectedForm?: string; // location.state.selectedForm value needed
skipDomPresenceFilter?: boolean; // Skip "all targets must exist" check
}
TourStepDefinition
interface TourStepDefinition {
target: string; // CSS selector, typically [data-tour="name"]
titleKey: string; // i18n key for step title
descriptionKey: string; // i18n key for step description
roles?: UniqueRoleName[];// Optional per-step role filter
side?: "top" | "bottom" | "left" | "right"; // Popover placement
}
TourStepState (persisted)
interface TourStepState {
completed: boolean;
completedAt?: string; // ISO timestamp
autoLaunchSeen: boolean; // Prevents re-triggering auto-launch
}
4. Step-by-Step: Adding a New Tour
Step 1: Extend the TourId union
In src/tours/types.ts:
export type TourId = "main-onboarding" | "sales-workflow" | "inventory-workflow";
// ^^^^^^^^^^^^^^^^^^^
Step 2: Create the tour definition file
Create src/tours/inventoryTour.ts:
import { UniqueRoleName } from "@flowpos-workspace/global/enums/role.enums";
import type { TourDefinition } from "./types";
export const inventoryTour: TourDefinition = {
id: "inventory-workflow",
nameKey: "tour.inventory.name",
descriptionKey: "tour.inventory.description",
roles: [
UniqueRoleName.Owner,
UniqueRoleName.Admin,
UniqueRoleName.StoreManager,
],
autoLaunch: false,
requiredPath: "/main",
// If your tour spans tabs:
// skipDomPresenceFilter: true,
// requiredMainSelectedForm: "/forms/inventory",
steps: [
{
target: '[data-tour="inventory-search"]',
titleKey: "tour.inventory.search.title",
descriptionKey: "tour.inventory.search.description",
side: "bottom",
},
{
target: '[data-tour="inventory-filters"]',
titleKey: "tour.inventory.filters.title",
descriptionKey: "tour.inventory.filters.description",
side: "bottom",
},
{
target: '[data-tour="inventory-add-product"]',
titleKey: "tour.inventory.addProduct.title",
descriptionKey: "tour.inventory.addProduct.description",
side: "left",
},
],
};
Step 3: Register the tour
In src/tours/index.ts:
import { inventoryTour } from "./inventoryTour";
export const tourRegistry: Record<TourId, TourDefinition> = {
"main-onboarding": mainTour,
"sales-workflow": salesTour,
"inventory-workflow": inventoryTour,
};
Step 4: Add data-tour attributes to target components
In your React components, add the data-tour attribute to the actual DOM element that should be highlighted:
// Direct HTML element — works immediately
<input data-tour="inventory-search" placeholder="Search products..." />
// Shadcn component — verify prop forwarding (see Section 6)
<Button data-tour="inventory-add-product">Add Product</Button>
Step 5: Add i18n translation keys
In src/i18n/locales/en.json:
{
"tour": {
"inventory": {
"name": "Inventory Tour",
"description": "Learn how to manage your product inventory",
"search": {
"title": "Product Search",
"description": "Use this search bar to quickly find products by name, SKU, or barcode."
},
"filters": {
"title": "Filters",
"description": "Narrow down your inventory view by category, stock level, or location."
},
"addProduct": {
"title": "Add New Product",
"description": "Click here to create a new product in your catalog."
}
}
}
}
Repeat for es.json (and any other locales).
Step 6: Wire up the Shepherd runner (if multi-tab)
If your tour is single-page with all targets visible, the existing Driver.js path in useTour handles it automatically — no runner changes needed.
If your tour spans tabs or has conditional UI, you need to:
- Create a runner function (or extend
shepherdRunner.ts) - Add a
STEP_TAB_MAPfor your tour's step-to-tab mapping - Define and export a custom event constant in
src/lib/constants.ts - Add an event listener in the target form component
- Route to the new runner in
useTour.tsstartTour()
See Section 5 for the full pattern.
Step 7: Test
Follow the Testing Checklist.
5. Handling Tabs, Modals & Conditional UI
This is the most complex part of the tour system. When a tour step targets an element that only exists when a specific tab is active (or a modal is open), you must:
5.1 The Problem
React tabs unmount/remount content. If Shepherd tries to attach to [data-tour="cart-area"] but the "items" tab isn't active, the element doesn't exist in the DOM.
5.2 The Solution: Custom Events + beforeShowPromise
Architecture:
shepherdRunner.ts React Form Component
│ │
│ 1. Dispatch CustomEvent │
│ { tab: "items" } │
├──────────────────────────────────────────→│
│ │ 2. setActiveTab("items")
│ │ 3. React re-renders
│ 4. waitForSelector('[data-tour="x"]') │
│ (polls DOM every 50ms) │
│ │
│ 5. Element found → step renders │
▼ ▼
Step-by-step implementation:
A. Define the tab map in your runner
// Maps data-tour value → tab name that must be active
const STEP_TAB_MAP: Record<string, string> = {
"customer-section": "customer",
"customer-next": "customer",
"cart-area": "items",
"payment-button": "checkout",
};
B. Define a custom event constant
In src/lib/constants.ts:
export const FLOWPOS_TOUR_SET_SALE_TAB = "flowpos-tour-set-sale-tab";
// For a new form:
export const FLOWPOS_TOUR_SET_INVENTORY_TAB = "flowpos-tour-set-inventory-tab";
C. Dispatch the event in beforeShowPromise
The runner's ensureTabAndWait function:
async function ensureTabAndWait(dataTour: string, selector: string): Promise<void> {
const requiredTab = STEP_TAB_MAP[dataTour];
if (requiredTab) {
// Tell React to switch tabs
window.dispatchEvent(
new CustomEvent(FLOWPOS_TOUR_SET_SALE_TAB, {
detail: { tab: requiredTab },
})
);
// Wait for React to commit the state change
await waitForDomAfterReactUpdate();
}
// Wait for the target element to appear and be visible
await waitForSelector(selector, 12000);
}
D. Listen in the React form component
// In your form component (e.g., SaleForm)
useEffect(() => {
const handler = (event: Event) => {
const { tab } = (event as CustomEvent<{ tab: string }>).detail;
if (["customer", "items", "checkout"].includes(tab)) {
setActiveTab(tab);
}
};
window.addEventListener(FLOWPOS_TOUR_SET_SALE_TAB, handler);
return () => window.removeEventListener(FLOWPOS_TOUR_SET_SALE_TAB, handler);
}, []);
E. Set skipDomPresenceFilter: true in the tour definition
This tells useTour not to filter out steps whose targets aren't currently in the DOM:
export const salesTour: TourDefinition = {
// ...
skipDomPresenceFilter: true,
// ...
};
5.3 Timing: waitForDomAfterReactUpdate()
This utility ensures React has committed its state update before we query the DOM:
function waitForDomAfterReactUpdate(): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => { // 1. Yield to microtask queue
requestAnimationFrame(() => { // 2. Wait for paint
requestAnimationFrame(() => { // 3. Wait for next paint (React commit)
resolve();
});
});
}, 0);
});
}
This is more reliable than a fixed setTimeout(500) because it ties to the actual browser rendering cycle.
5.4 Handling Modals
For steps targeting elements inside modals:
- Open the modal in
beforeShowPromise(dispatch a custom event or call a function) - Wait for the modal's content to mount with
waitForSelector - Optionally close the modal when moving to the next step (use Shepherd's
hideevent)
// Example: open a modal before a step
tour.addStep({
id: "modal-content",
attachTo: { element: '[data-tour="modal-field"]', on: "bottom" },
beforeShowPromise: async () => {
window.dispatchEvent(new CustomEvent("tour-open-modal", { detail: { modal: "discount" } }));
await waitForSelector('[data-tour="modal-field"]', 8000);
},
// ...
});
6. DOM Anchoring with data-tour
6.1 The Pattern
All tour step targets use data-tour attributes as CSS selectors:
<div data-tour="customer-section">...</div>
Selector in step definition:
target: '[data-tour="customer-section"]'
6.2 Naming Convention
Use kebab-case descriptive names that indicate the feature area:
nav-sales — Navigation: sales section
new-sale-button — Action: create new sale
customer-section — Area: customer form section
payment-button — Action: complete payment
inventory-search — Feature: inventory search bar
6.3 Ensuring Props Reach the DOM
HTML elements always accept data-* attributes — no issues.
Shadcn/ui components — most forward props to the underlying DOM element via React.forwardRef and spread ...props. Verify by inspecting the rendered DOM.
Components that may NOT forward data-*:
| Component | Forwards? | Workaround |
|---|---|---|
<Button> | Yes | Direct usage works |
<Input> | Yes | Direct usage works |
<Select> | Partial | Wrap in a <div data-tour="..."> |
<Dialog> | No (portal) | Place data-tour on trigger or wrap content |
<Tabs> | Partial | Place data-tour on TabsContent children |
| Custom wrappers | Varies | Check if ...rest is spread to the root element |
Verification method:
// In browser DevTools:
document.querySelector('[data-tour="your-target"]')
// Should return the element. If null, the prop isn't reaching the DOM.
6.4 Wrapper Pattern for Stubborn Components
When a component swallows data-tour, wrap it:
// Instead of:
<Select data-tour="category-filter" />
// Use:
<div data-tour="category-filter">
<Select />
</div>
This ensures the highlight covers the component while the selector always resolves.
7. Shepherd Runner Internals
7.1 Tour Instance Configuration
const tour = new Shepherd.Tour({
tourName: tourId,
useModalOverlay: true, // Dark backdrop behind highlighted element
exitOnEsc: true, // ESC key cancels tour
keyboardNavigation: false, // Disable arrow key navigation (conflicts with form inputs)
defaultStepOptions: {
canClickTarget: false, // Prevent clicking highlighted element during tour
modalOverlayOpeningPadding: 8, // Space around highlighted element
modalOverlayOpeningRadius: 8, // Rounded corners for highlight cutout
scrollTo: {
behavior: "smooth",
block: "nearest",
inline: "nearest",
},
},
});
7.2 Step Anatomy
Each step added to the tour:
tour.addStep({
id: "customer-section", // Unique step ID
title: t("tour.sales.customerSection.title"), // Translated title
text: stepDescriptionHtml(t, descriptionKey, idx, total), // HTML with progress
attachTo: { element: '[data-tour="customer-section"]', on: "bottom" },
beforeShowPromise: () => ensureTabAndWait("customer-section", selector),
buttons: [
{ text: "Previous", action() { tour.back(); }, secondary: true },
{ text: "Next", action() { tour.next(); } },
],
});
7.3 Progress Display
Steps show "Step X of Y" as an HTML paragraph above the description:
function stepDescriptionHtml(t, descriptionKey, stepIndex, totalSteps): string {
const progress = t("tour.common.progress", { current: stepIndex + 1, total: totalSteps });
return `<p class="shepherd-step-progress">${progress}</p><p>${description}</p>`;
}
7.4 Element Visibility Check
Before attaching to an element, the runner verifies it's actually visible (not hidden or zero-size):
function isTargetAttachable(selector: string): boolean {
const el = document.querySelector(selector);
if (!el) return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
7.5 Retry Logic
If a step's target isn't found after tab switching, the runner retries once:
try {
await switchTabAndWaitForTarget();
} catch {
// Retry once
try {
await switchTabAndWaitForTarget();
} catch {
console.warn(`Element not found — step may show centered`);
}
}
If the element still isn't found, Shepherd shows the popover centered (no highlight) rather than crashing.
8. i18n Integration
8.1 Key Structure
All tour translations live under the tour namespace:
tour.
├── common. # Shared button labels
│ ├── next
│ ├── prev
│ ├── done
│ ├── skip
│ └── progress # "Step {{current}} of {{total}}"
├── <tourName>. # Per-tour metadata
│ ├── name
│ ├── description
│ └── <stepId>. # Per-step content
│ ├── title
│ └── description
└── launcher. # TourLauncher UI
├── title
├── description
├── start
├── reset
├── completed
└── notCompleted
8.2 Adding Translations
Always add to both en.json and es.json (at minimum):
src/i18n/locales/en.json
src/i18n/locales/es.json
8.3 Interpolation
The progress string uses i18next interpolation:
"progress": "Step {{current}} of {{total}}"
Usage in code:
t("tour.common.progress", { current: 2, total: 5 })
// → "Step 2 of 5"
9. Persistence & State Management
9.1 Storage Key
flowpos:tours:{userId}:{businessId}
Example: flowpos:tours:abc123:biz456
9.2 Storage Shape
{
"main-onboarding": {
"completed": true,
"completedAt": "2026-03-25T10:30:00.000Z",
"autoLaunchSeen": true
},
"sales-workflow": {
"completed": false,
"autoLaunchSeen": false
}
}
9.3 State Transitions
| Event | State Change |
|---|---|
| Tour completes (Done button) | completed: true, completedAt: <now>, autoLaunchSeen: true |
| Tour cancelled (ESC or X) | Same as complete (intentional — don't nag the user) |
| Auto-launch fires | autoLaunchSeen: true (before tour starts, prevents re-trigger) |
| User clicks "Reset" | completed: false, completedAt: undefined |
9.4 Why Cancel = Complete
Both onDone and onCancel mark the tour as completed. This is intentional — if a user dismisses a tour, they likely don't want it to keep re-appearing. They can always re-launch from the TourLauncher.
10. Role-Based Filtering
10.1 Tour-Level Roles
The roles array on TourDefinition controls which roles can see the tour in the launcher:
roles: [UniqueRoleName.Owner, UniqueRoleName.Admin, UniqueRoleName.StoreManager]
A Cashier won't see this tour in the TourLauncher dropdown.
10.2 Step-Level Roles
Individual steps can have their own roles array. Steps are filtered out for users who don't match:
{
target: '[data-tour="nav-reports"]',
titleKey: "tour.main.navReports.title",
descriptionKey: "tour.main.navReports.description",
roles: [UniqueRoleName.Owner, UniqueRoleName.Admin], // Cashiers skip this step
}
10.3 Filtering Order
useTour.startTour()filters steps bycurrentRole- If
skipDomPresenceFilteris false, further filters by DOM element existence - If zero steps remain after filtering, the tour doesn't start
11. Styling & Theming
11.1 CSS File
All tour styles live in src/tours/tours.css, imported by useTour.ts.
11.2 Design Tokens
The styles use Tailwind CSS custom properties (HSL format) to support light/dark mode:
.shepherd-content {
background-color: hsl(var(--popover));
color: hsl(var(--popover-foreground));
border: 1px solid hsl(var(--border));
}
11.3 Key Dimensions
| Property | Value |
|---|---|
| Max width | 340px (desktop), 100vw - 2rem (mobile) |
| Padding | 1.25rem (desktop), 1rem (mobile < 480px) |
| Border radius | 0.75rem |
| Overlay z-index | 10000 |
| Popover z-index | 10001 |
| Modal overlay opening padding | 8px |
| Modal overlay opening radius | 8px |
11.4 Responsive Breakpoints
@media (max-width: 768px) { /* Tablet adjustments */ }
@media (max-width: 480px) { /* Phone adjustments — smaller padding/font */ }
11.5 Matching Driver.js and Shepherd.js
Both libraries are styled to look identical. If you add new CSS for Shepherd, consider adding the equivalent for Driver.js (and vice versa) to maintain visual consistency.
12. Troubleshooting
Tour doesn't start
| Symptom | Cause | Fix |
|---|---|---|
startTour() does nothing | Zero steps after role filtering | Check user's role vs. step roles |
startTour() does nothing | Zero steps after DOM filtering | Check data-tour attributes exist in DOM, or set skipDomPresenceFilter: true |
| Tour starts but no highlight | Element has display: none or zero dimensions | Ensure element is visible when step activates |
Step shows centered (no highlight)
This means waitForSelector timed out — the element wasn't found within 12 seconds.
- Check if the tab-switching event is being dispatched correctly
- Verify the event listener in the form component is wired up
- Check that
STEP_TAB_MAPmaps the step'sdata-tourvalue to the correct tab name - Inspect the DOM to confirm the element exists after tab switch
Auto-launch doesn't fire
- Check
autoLaunch: truein the tour definition - Check that
autoLaunchSeenisn't alreadytruein localStorage - Verify the user's role is in the tour's
rolesarray - Confirm at least one step's target exists in the DOM at launch time
data-tour attribute not in DOM
- The component may not forward
data-*props — wrap it in a<div>(see Section 6.4) - The component may be conditionally rendered — ensure it's mounted
Tour overlay blocks interaction after cancel
- Ensure
onCancelcallback is wired:tour.on("cancel", () => onCancel()) - Check that
driverRef.current?.destroy()andshepherdRef.current?.cancel()are called on cleanup
Multiple tours interfere
The hook destroys any existing tour instance before starting a new one:
driverRef.current?.destroy();
void shepherdRef.current?.cancel();
If you see overlapping tours, ensure you're going through useTour.startTour() and not creating Shepherd instances directly.
13. Testing Checklist
Manual Testing
- Launcher visibility — TourLauncher button appears in header for authenticated users
- Tour listing — All role-appropriate tours appear in the dropdown
- Start from launcher — Tour starts and first step highlights correctly
- Auto-launch — Tour auto-starts on first visit (if
autoLaunch: true) - Auto-launch once — Refreshing the page does NOT re-trigger auto-launch
- Step navigation — Next/Previous buttons work correctly
- Step highlighting — Each step highlights the correct DOM element
- Tab switching — For multi-tab tours, tabs switch automatically before each step
- Popover placement — Popover appears on the correct side of the element
- Scroll to element — Off-screen elements are scrolled into view smoothly
- ESC to cancel — Pressing ESC dismisses the tour
- Done button — Final step shows "Done" instead of "Next"
- Completion persists — After completing, the tour shows as "Completed" in launcher
- Reset — Clicking "Reset" in launcher allows re-starting the tour
- Role filtering — Users with different roles see different steps
- Dark mode — Tour styling adapts correctly to dark theme
- Mobile responsive — Popover fits within viewport on small screens
- Navigation required — If on wrong route, launcher navigates before starting
- Unmount cleanup — Navigating away mid-tour doesn't leave overlay artifacts
localStorage Verification
// In browser DevTools console:
const key = Object.keys(localStorage).find(k => k.startsWith("flowpos:tours:"));
JSON.parse(localStorage.getItem(key));
// Should show tour states with correct completed/autoLaunchSeen values
// To reset all tour state for testing:
Object.keys(localStorage)
.filter(k => k.startsWith("flowpos:tours:"))
.forEach(k => localStorage.removeItem(k));
14. Prompt Pack for AI-Assisted Development
Use these prompts when asking an AI assistant (Claude, Cursor, etc.) to help integrate a new tour. Replace [FormName], [route], and element descriptions with your specifics.
Prompt 1: Full Tour Integration (single prompt for simple forms)
Add a Shepherd.js tour for [FormName] at route [route].
Use the existing pattern: TourDefinition in src/tours/, register in tourRegistry,
useTour hook, TourLauncher, and data-tour attributes.
Target these elements:
1. [element description] → data-tour="[name]"
2. [element description] → data-tour="[name]"
3. [element description] → data-tour="[name]"
The form has [no tabs / tabs: tab1, tab2, tab3 / a modal for X].
Roles: [Owner, Admin, StoreManager].
Auto-launch: [yes/no].
Ensure data-tour attrs reach the DOM (wrap Shadcn components if needed).
Add i18n keys to both en.json and es.json.
Prompt 2: DOM Anchor Audit (before defining steps)
In [file path], find where each of these UI elements is rendered and confirm
that a data-tour attribute on each one will reach the actual DOM element
(not be swallowed by a wrapper component):
1. [element description]
2. [element description]
3. [element description]
For each, tell me:
- The exact JSX line where I should add data-tour="[name]"
- Whether the component forwards data-* props to the DOM
- If it doesn't, suggest a wrapper approach
- Which tab/state must be active for the element to exist
Prompt 3: Multi-Tab Tour (complex forms)
Create a Shepherd.js tour for [FormName] that spans these tabs: [tab1, tab2, tab3].
Steps:
1. [step on tab1] → data-tour="[name]"
2. [step on tab2] → data-tour="[name]"
3. [step on tab3] → data-tour="[name]"
Follow the existing pattern in shepherdRunner.ts:
- Add a STEP_TAB_MAP for this tour
- Define a custom event constant (like FLOWPOS_TOUR_SET_SALE_TAB)
- Add the event listener in [FormComponent]
- Wire up the runner in useTour.ts startTour()
- Set skipDomPresenceFilter: true
- Use beforeShowPromise with ensureTabAndWait pattern
Prompt 4: Tour Teardown & Edge Cases
Review the tour integration for [tourId] and verify:
1. Cleanup on component unmount (no leaked overlays)
2. Cleanup on route change mid-tour
3. What happens if the user resizes the window during the tour
4. What happens if the target element is removed by async data loading
5. That the tour handles the case where the form has no data yet (empty state)
Add any missing cleanup or guard logic.
Prompt 5: i18n + Step Copywriting
Write the i18n entries for a [FormName] tour with these steps:
1. [step description and purpose]
2. [step description and purpose]
3. [step description and purpose]
Follow the existing key structure: tour.[tourName].[stepId].title/description
Write in both English and Spanish.
Keep descriptions concise (1-2 sentences) and action-oriented.
Include the tour name and description keys for the launcher.
Appendix: Quick Reference
Adding a Tour (Minimal Steps)
- Add ID to
TourIdunion intypes.ts - Create
src/tours/[name]Tour.tswithTourDefinition - Register in
src/tours/index.ts - Add
data-tourattributes to target components - Add i18n keys to
en.jsonandes.json - (If multi-tab) Add tab map + custom event + listener + runner logic
Key Files to Touch
| What | File |
|---|---|
| Tour ID type | src/tours/types.ts |
| Tour definition | src/tours/[name]Tour.ts |
| Registry | src/tours/index.ts |
| Runner (multi-tab only) | src/tours/shepherdRunner.ts |
| Hook routing (Shepherd only) | src/hooks/useTour.ts |
| Constants (multi-tab only) | src/lib/constants.ts |
| Translations | src/i18n/locales/en.json, es.json |
| CSS (rarely) | src/tours/tours.css |
| Target components | Wherever the highlighted elements live |
Shepherd.js Version
15.2.2 — see apps/frontend-pwa/package.json
Driver.js Version
1.4.0 — used for simpler tours (main-onboarding)