OnboardingModule — auto-run a tour on first visit
Wraps a regular
ScriptSkillwith a persisted "seen" flag, optional URL-based gating, and a palette command to re-run the tour. The first time a user lands on a matching page, the tour fires automatically; on subsequent visits it stays silent unless reset.
Opt-in. The module is just a thin lifecycle around a skill you've already defined — no DOM of its own.
When to use
- A guided tour explaining where dddk's Ctrl+K, voice gesture, and dock live
- Per-feature first-run notices ("hey, you've enabled InlineAgent — here's what it does")
- New-version "what's changed" pop-ups gated to one URL
Don't use when:
- The tour needs to run every session (e.g. recurring training reminders) — use a normal palette command instead
- You need branching steps based on the user's account state — write a
PanelSkilland run it conditionally yourself - The "first-time" thing isn't a tour but a single toast — fire
dddk.subtitle.show()from your bootstrap code
Import
import { OnboardingModule } from '@perhapxin/dddk';
import type { OnboardingConfig } from '@perhapxin/dddk';
Minimum setup
import { OnboardingModule, type ScriptSkill } from '@perhapxin/dddk';
// 1. Define the tour as a regular ScriptSkill
const tour: ScriptSkill = {
id: 'first-time-tour',
name: '快速導覽',
kind: 'script',
steps: [
{ kind: 'subtitle', text: '歡迎!按 Ctrl+K 隨時叫出指令面板。', autoHide: 4000 },
{ kind: 'subtitle', text: '長按空白鍵可以對 dddk 講話。', autoHide: 4000 },
{ kind: 'subtitle', text: '長按頁面上任何元素 → 它會被鎖定成下個操作的 context。', autoHide: 4000 },
],
};
// 2. Register it with your skill registry as usual
dddk.skills.register(tour);
// 3. Wrap it in OnboardingModule
const onboarding = new OnboardingModule({
skill: tour,
storage: localStorageAdapter, // your StorageAdapter
runOnPath: ['/', '/dashboard'], // only auto-run on these paths
startDelayMs: 800, // wait for host to finish rendering
});
// 4. After dddk.mount(), let it decide whether to run
dddk.mount();
onboarding.maybeRun((id) => dddk.skills.run(id));
// 5. Add palette commands so the user can re-watch
onboarding.registerOn(dddk.palette, (id) => dddk.skills.run(id));
Configuration
| Option | Default | Description |
|---|---|---|
skill |
— | The ScriptSkill to run. Required. |
storage |
— | StorageAdapter for the "seen" flag. Required. |
storageKey |
dddk.onboarding.seen.<skill.id> |
Where the flag lives. |
runOnPath |
any page | string | RegExp | Array<string | RegExp> — gate by location.pathname. |
startDelayMs |
500 |
Wait this long after maybeRun() before starting the skill. |
Runtime API
onboarding.maybeRun(runSkill); // auto-run if not seen + path matches
onboarding.markSeen(); // mark seen without running (e.g. after user manually completes)
onboarding.reset(); // forget the flag so the tour runs again next visit
onboarding.hasSeen(); // boolean
onboarding.paletteCommands(run); // returns PaletteItem[] for manual registration
onboarding.registerOn(palette, run); // shortcut: registers commands on a palette
The auto-registered palette command is 重看導覽 — calling it resets the seen flag and immediately re-runs the tour.
URL gating
runOnPath accepts mixed strings and regexes:
new OnboardingModule({
skill: tour,
storage,
runOnPath: [
'/',
/^\/projects\/[^/]+$/, // any single project page
/^\/onboarding/, // every page under /onboarding
],
});
Strings match exactly; regexes use .test(). The check runs on every maybeRun() call, so route changes that re-call it will re-evaluate.
Storage shape
interface StorageAdapter {
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
}
Any KV that satisfies this works. The default key is namespaced by skill id so multiple onboarding modules don't collide. Override with storageKey if you need cross-version invalidation (e.g. dddk.onboarding.seen.v2.tour).
Two ways to "complete"
The auto-run path calls markSeen() after the skill resolves — so a skill that throws does not mark seen, and the user gets another chance next visit. If your skill should mark seen even on partial completion (e.g. the user dismissed mid-tour), call markSeen() from inside the skill's step handlers.
Tear-down
There's nothing to clean up — OnboardingModule holds no listeners or DOM. The palette commands stay registered until you call palette.removeItem(id) or destroy the palette.
See also: ../skills/how-to-add-a-skill.md for the underlying ScriptSkill shape, ./overview.md for the module index.