Onboarding Architect
Architecture
packages/shared-onboarding/ # Reusable engine (no app imports)
├── src/
│ ├── types.ts # Tour, OnboardingStep, Placement, Align
│ ├── OnboardingProvider.tsx # React context: useReducer state machine
│ ├── useOnboarding.ts # Consumer hook: startTour, nextStep, etc.
│ ├── ShowcaseModal.tsx # Fullscreen modal (lazy-loaded)
│ ├── GuidedOverlay.tsx # Spotlight overlay (lazy-loaded)
│ ├── GuidedTooltip.tsx # Radix Popover tooltip (side, align, sideOffset)
│ ├── storage.ts # localStorage: markTourCompleted, isTourCompleted
│ └── index.ts # Public exports
apps/raamattu-nyt/src/onboarding/ # App-level tour definitions
├── tours/
│ ├── showcaseHome.ts # Showcase tour (7 steps, i18n keys + CTA)
│ ├── searchBasics.ts # Guided tour: search basics (4 steps)
│ ├── searchQuestionsTab.ts # Guided tour: Q&A tab (4 steps)
│ └── index.ts # useAllTours() hook: resolves i18n + images + overrides
apps/raamattu-nyt/src/hooks/
└── useOnboardingImages.ts # Fetches images + guided overrides from app_config
apps/raamattu-nyt/src/pages/
└── AdminOnboardingPage.tsx # Admin: tour list, image upload, guided placement
Tour Modes
| Mode | Component | Behavior |
|------|-----------|----------|
| showcase | ShowcaseModal | Fullscreen modal, image per step, CTA buttons, keyboard/touch nav |
| guided | GuidedOverlay + GuidedTooltip | Spotlight on [data-onboard="..."] target, Radix Popover tooltip |
Adding a New Showcase Tour
- Create
apps/raamattu-nyt/src/onboarding/tours/myTour.ts:
import type { Tour } from "@shared-onboarding";
interface ShowcaseStepDef {
id: string;
titleKey: string;
descriptionKey: string;
ctaKey?: string;
ctaHref?: string;
}
const myTourDef: ShowcaseStepDef[] = [
{
id: "step1",
titleKey: "onboarding.showcase.myTour.step1.title",
descriptionKey: "onboarding.showcase.myTour.step1.description",
},
];
export function createMyTour(
images?: Record<string, string>,
t?: (key: string) => string,
): Tour {
const tr = t ?? ((key: string) => key);
return {
id: "my-tour",
mode: "showcase",
steps: myTourDef.map((def) => ({
id: def.id,
title: tr(def.titleKey),
description: tr(def.descriptionKey),
image: images?.[def.id],
cta: def.ctaKey ? { label: tr(def.ctaKey), href: def.ctaHref } : undefined,
})),
};
}
export const myTour: Tour = createMyTour();
- Add i18n keys to
public/locales/fi/common.jsonandpublic/locales/en/common.json - Register in
tours/index.ts— import, add touseAllTours()return array - Upload images via admin panel or set in
app_configkeyonboarding_showcase_images
Adding a New Guided Tour
- Create step definitions with
GuidedStepDef:
import type { GuidedStepDef } from "./searchBasics";
export const myGuidedDef: { id: string; mode: "guided"; steps: GuidedStepDef[] } = {
id: "my-guided-tour",
mode: "guided",
steps: [
{
id: "first-element",
titleKey: "onboarding.guided.myTour.first.title",
descriptionKey: "onboarding.guided.myTour.first.description",
target: '[data-onboard="my-first-element"]',
placement: "bottom",
align: "center",
sideOffset: 12,
},
],
};
- Add
data-onboard="my-first-element"attribute to target DOM elements - Add i18n keys to locale files
- Register in
tours/index.ts:- Import def into
guidedTourDefsarray - Add static fallback to
allToursarray
- Import def into
Trigger Points
| Trigger | How it works |
|---------|-------------|
| startTour(tourId) | Direct call via useOnboarding() hook |
| URL param ?tour=tourId | SearchPage detects param in useEffect, calls startTour with 500ms delay |
| UserMenu dropdown | Items in "Tutustu ominaisuuksiin" section call startTour or navigate |
| Button/link | Navigate to page with ?tour=tourId query param |
To add a URL-triggered tour to any page:
const [searchParams] = useSearchParams();
const { startTour } = useOnboarding();
const tourTriggered = useRef(false);
useEffect(() => {
const tourId = searchParams.get("tour");
if (tourId && !tourTriggered.current) {
tourTriggered.current = true;
setTimeout(() => startTour(tourId), 500);
}
}, []);
Admin Overrides (app_config)
Two app_config keys control dynamic behavior:
| Key | Shape | Purpose |
|-----|-------|---------|
| onboarding_showcase_images | { "tour-id": { "step-id": "https://url" } } | Showcase step images |
| onboarding_guided_overrides | { "tour-id": { "step-id": { placement?, align?, sideOffset? } } } | Guided tooltip positioning |
Admin panel at /ohjaamo/onboarding provides UI for both.
Supabase Storage
Bucket: onboarding-images (public read, authenticated write)
Path pattern: onboarding/{tourId}/{stepId}.webp
Key Types
// packages/shared-onboarding/src/types.ts
type TourMode = "showcase" | "guided";
type Placement = "top" | "bottom" | "left" | "right";
type Align = "start" | "center" | "end";
interface OnboardingStep {
id: string;
title: string;
description: string;
image?: string; // Showcase only
target?: string; // Guided only: CSS selector
placement?: Placement; // Guided only
align?: Align; // Guided only
sideOffset?: number; // Guided only (default 12)
cta?: { label: string; href?: string; action?: () => void };
beforeStep?: () => void | Promise<void>;
}
interface Tour { id: string; mode: TourMode; steps: OnboardingStep[] }
Resolution Pipeline
useAllTours() in tours/index.ts resolves tours at runtime:
- i18n: Translation keys resolved via
useTranslation("common") - Images:
useOnboardingImages(tourId)mergesapp_configimages into showcase steps - Overrides:
useGuidedOverrides()mergesapp_configplacement/align/sideOffset into guided steps
Code defaults are always fallback — admin overrides win when present.
State Machine
idle → START(tourId) → active { tourId, stepIndex: 0 }
→ NEXT → stepIndex++ (or COMPLETE if last)
→ PREV → stepIndex-- (min 0)
→ SKIP/COMPLETE → completed { tourId } → idle
Completion persisted to localStorage: onboarding_completed:{tourId}.
References
- architecture.md — Complete file map, data flow, existing tours, React Query keys