torchlit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ function deepQuery(selector, root = document.body) {
2
+ const found = root.querySelector(selector);
3
+ if (found) return found;
4
+ const children = root.querySelectorAll("*");
5
+ for (const el of children) {
6
+ if (el.shadowRoot) {
7
+ const shadowResult = deepQuery(selector, el.shadowRoot);
8
+ if (shadowResult) return shadowResult;
9
+ }
10
+ }
11
+ return null;
12
+ }
13
+ const DEFAULT_STORAGE_KEY = "torchlit-state";
14
+ const DEFAULT_TARGET_ATTR = "data-tour-id";
15
+ const DEFAULT_SPOTLIGHT_PADDING = 10;
16
+ const noopStorage = {
17
+ getItem: () => null,
18
+ setItem: () => {
19
+ }
20
+ };
21
+ function defaultStorage() {
22
+ try {
23
+ const test = "__torchlit_test__";
24
+ localStorage.setItem(test, test);
25
+ localStorage.removeItem(test);
26
+ return localStorage;
27
+ } catch {
28
+ return noopStorage;
29
+ }
30
+ }
31
+ class TourService {
32
+ constructor(config = {}) {
33
+ this.tours = /* @__PURE__ */ new Map();
34
+ this.activeTourId = null;
35
+ this.currentStepIndex = 0;
36
+ this.listeners = /* @__PURE__ */ new Set();
37
+ this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
38
+ this.storage = config.storage ?? defaultStorage();
39
+ this.targetAttribute = config.targetAttribute ?? DEFAULT_TARGET_ATTR;
40
+ this.spotlightPadding = config.spotlightPadding ?? DEFAULT_SPOTLIGHT_PADDING;
41
+ this.persistedState = this.loadState();
42
+ }
43
+ /* ── Persistence ──────────────────────────────── */
44
+ loadState() {
45
+ try {
46
+ const stored = this.storage.getItem(this.storageKey);
47
+ if (stored) {
48
+ const parsed = JSON.parse(stored);
49
+ return {
50
+ completed: Array.isArray(parsed.completed) ? parsed.completed : [],
51
+ dismissed: Array.isArray(parsed.dismissed) ? parsed.dismissed : []
52
+ };
53
+ }
54
+ } catch (error) {
55
+ console.error("[torchlit] Failed to load state:", error);
56
+ }
57
+ return { completed: [], dismissed: [] };
58
+ }
59
+ saveState() {
60
+ try {
61
+ this.storage.setItem(this.storageKey, JSON.stringify(this.persistedState));
62
+ } catch (error) {
63
+ console.error("[torchlit] Failed to save state:", error);
64
+ }
65
+ }
66
+ register(input) {
67
+ if (Array.isArray(input)) {
68
+ input.forEach((t) => this.tours.set(t.id, t));
69
+ } else {
70
+ this.tours.set(input.id, input);
71
+ }
72
+ }
73
+ /* ── Queries ──────────────────────────────────── */
74
+ /** Return a registered tour by ID. */
75
+ getTour(id) {
76
+ return this.tours.get(id);
77
+ }
78
+ /** Return all registered tours. */
79
+ getAvailableTours() {
80
+ return Array.from(this.tours.values());
81
+ }
82
+ /**
83
+ * Whether a `first-visit` tour should auto-start.
84
+ * Returns `false` if the tour is manual, already completed, or dismissed.
85
+ */
86
+ shouldAutoStart(tourId) {
87
+ const tour = this.tours.get(tourId);
88
+ if (!tour || tour.trigger !== "first-visit") return false;
89
+ return !this.persistedState.completed.includes(tourId) && !this.persistedState.dismissed.includes(tourId);
90
+ }
91
+ /** Whether any tour is currently active. */
92
+ isActive() {
93
+ return this.activeTourId !== null;
94
+ }
95
+ /* ── Tour control ─────────────────────────────── */
96
+ /** Start a tour by ID. No-op if the tour doesn't exist or has no steps. */
97
+ start(tourId) {
98
+ const tour = this.tours.get(tourId);
99
+ if (!tour || tour.steps.length === 0) return;
100
+ this.activeTourId = tourId;
101
+ this.currentStepIndex = 0;
102
+ this.notify();
103
+ }
104
+ /** Advance to the next step, or complete the tour if on the last step. */
105
+ nextStep() {
106
+ if (!this.activeTourId) return;
107
+ const tour = this.tours.get(this.activeTourId);
108
+ if (this.currentStepIndex < tour.steps.length - 1) {
109
+ this.currentStepIndex++;
110
+ this.notify();
111
+ } else {
112
+ this.completeTour();
113
+ }
114
+ }
115
+ /** Go back to the previous step. No-op if already on step 0. */
116
+ prevStep() {
117
+ if (!this.activeTourId) return;
118
+ if (this.currentStepIndex > 0) {
119
+ this.currentStepIndex--;
120
+ this.notify();
121
+ }
122
+ }
123
+ /** Skip / dismiss the current tour. Persists "dismissed" state. */
124
+ skipTour() {
125
+ if (!this.activeTourId) return;
126
+ const id = this.activeTourId;
127
+ const tour = this.tours.get(id);
128
+ if (!this.persistedState.dismissed.includes(id)) {
129
+ this.persistedState.dismissed.push(id);
130
+ this.saveState();
131
+ }
132
+ this.activeTourId = null;
133
+ this.currentStepIndex = 0;
134
+ this.notify();
135
+ tour?.onSkip?.();
136
+ }
137
+ completeTour() {
138
+ if (!this.activeTourId) return;
139
+ const id = this.activeTourId;
140
+ const tour = this.tours.get(id);
141
+ if (!this.persistedState.completed.includes(id)) {
142
+ this.persistedState.completed.push(id);
143
+ this.saveState();
144
+ }
145
+ this.activeTourId = null;
146
+ this.currentStepIndex = 0;
147
+ this.notify();
148
+ tour?.onComplete?.();
149
+ }
150
+ /* ── Snapshot (current state for overlay) ─────── */
151
+ /** Return a snapshot of the current tour state, or `null` if inactive. */
152
+ getSnapshot() {
153
+ if (!this.activeTourId) return null;
154
+ const tour = this.tours.get(this.activeTourId);
155
+ if (!tour) return null;
156
+ const step = tour.steps[this.currentStepIndex];
157
+ if (!step) return null;
158
+ const targetElement = this.findTarget(step.target);
159
+ const targetRect = targetElement?.getBoundingClientRect() ?? null;
160
+ return {
161
+ tourId: this.activeTourId,
162
+ tourName: tour.name,
163
+ step,
164
+ stepIndex: this.currentStepIndex,
165
+ totalSteps: tour.steps.length,
166
+ targetRect,
167
+ targetElement
168
+ };
169
+ }
170
+ /* ── Shadow DOM target resolution ─────────────── */
171
+ /** Find a DOM element by its `data-tour-id` (or custom attribute). */
172
+ findTarget(targetId) {
173
+ return deepQuery(`[${this.targetAttribute}="${targetId}"]`, document.body);
174
+ }
175
+ /* ── Observer pattern ─────────────────────────── */
176
+ /** Subscribe to snapshot changes. Returns an unsubscribe function. */
177
+ subscribe(listener) {
178
+ this.listeners.add(listener);
179
+ return () => this.listeners.delete(listener);
180
+ }
181
+ notify() {
182
+ const snapshot = this.getSnapshot();
183
+ this.listeners.forEach((listener) => listener(snapshot));
184
+ }
185
+ /* ── Reset (for testing & demos) ──────────────── */
186
+ /** Clear all persisted state and stop any active tour. */
187
+ resetAll() {
188
+ this.persistedState = { completed: [], dismissed: [] };
189
+ this.activeTourId = null;
190
+ this.currentStepIndex = 0;
191
+ this.tours.clear();
192
+ this.saveState();
193
+ this.notify();
194
+ }
195
+ }
196
+ function createTourService(config) {
197
+ return new TourService(config);
198
+ }
199
+ export {
200
+ TourService as T,
201
+ createTourService as c,
202
+ deepQuery as d
203
+ };
204
+ //# sourceMappingURL=tour-service-BKz7eeWb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-service-BKz7eeWb.js","sources":["../src/utils/deep-query.ts","../src/tour-service.ts"],"sourcesContent":["/**\n * Recursively search the DOM — including shadow roots — for an element\n * matching the given CSS selector.\n *\n * This is the key differentiator vs. libraries like Shepherd.js or Intro.js\n * which cannot pierce shadow DOM boundaries.\n *\n * @param selector A valid CSS selector string.\n * @param root The root element (or Document) to start searching from.\n * Defaults to `document.body`.\n * @returns The first matching `Element`, or `null`.\n *\n * @example\n * ```ts\n * import { deepQuery } from 'torchlit';\n *\n * const el = deepQuery('[data-tour-id=\"sidebar-nav\"]');\n * ```\n */\nexport function deepQuery(\n selector: string,\n root: Element | Document = document.body,\n): Element | null {\n // Try light DOM first (fast path)\n const found = root.querySelector(selector);\n if (found) return found;\n\n // Walk children that expose a shadowRoot\n const children = root.querySelectorAll('*');\n for (const el of children) {\n if (el.shadowRoot) {\n const shadowResult = deepQuery(selector, el.shadowRoot as unknown as Document);\n if (shadowResult) return shadowResult;\n }\n }\n\n return null;\n}\n","import { deepQuery } from './utils/deep-query.js';\nimport type {\n TourConfig,\n TourDefinition,\n TourListener,\n TourSnapshot,\n TourState,\n StorageAdapter,\n} from './types.js';\n\n// ── Defaults ─────────────────────────────────────────────────────────────────\n\nconst DEFAULT_STORAGE_KEY = 'torchlit-state';\nconst DEFAULT_TARGET_ATTR = 'data-tour-id';\nconst DEFAULT_SPOTLIGHT_PADDING = 10;\n\n/** A no-op storage adapter for SSR / environments without localStorage. */\nconst noopStorage: StorageAdapter = {\n getItem: () => null,\n setItem: () => {},\n};\n\n/** Safely wrap localStorage — falls back to noop if unavailable. */\nfunction defaultStorage(): StorageAdapter {\n try {\n // Guard against SSR or restricted environments\n const test = '__torchlit_test__';\n localStorage.setItem(test, test);\n localStorage.removeItem(test);\n return localStorage;\n } catch {\n return noopStorage;\n }\n}\n\n// ── TourService ──────────────────────────────────────────────────────────────\n\nexport class TourService {\n private tours: Map<string, TourDefinition> = new Map();\n private persistedState: TourState;\n private activeTourId: string | null = null;\n private currentStepIndex = 0;\n private listeners: Set<TourListener> = new Set();\n\n // Resolved config\n private readonly storageKey: string;\n private readonly storage: StorageAdapter;\n private readonly targetAttribute: string;\n readonly spotlightPadding: number;\n\n constructor(config: TourConfig = {}) {\n this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;\n this.storage = config.storage ?? defaultStorage();\n this.targetAttribute = config.targetAttribute ?? DEFAULT_TARGET_ATTR;\n this.spotlightPadding = config.spotlightPadding ?? DEFAULT_SPOTLIGHT_PADDING;\n this.persistedState = this.loadState();\n }\n\n /* ── Persistence ──────────────────────────────── */\n\n private loadState(): TourState {\n try {\n const stored = this.storage.getItem(this.storageKey);\n if (stored) {\n const parsed = JSON.parse(stored);\n return {\n completed: Array.isArray(parsed.completed) ? parsed.completed : [],\n dismissed: Array.isArray(parsed.dismissed) ? parsed.dismissed : [],\n };\n }\n } catch (error) {\n console.error('[torchlit] Failed to load state:', error);\n }\n return { completed: [], dismissed: [] };\n }\n\n private saveState(): void {\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(this.persistedState));\n } catch (error) {\n console.error('[torchlit] Failed to save state:', error);\n }\n }\n\n /* ── Registration ─────────────────────────────── */\n\n /** Register a single tour definition. */\n register(tours: TourDefinition[]): void;\n register(tour: TourDefinition): void;\n register(input: TourDefinition | TourDefinition[]): void {\n if (Array.isArray(input)) {\n input.forEach(t => this.tours.set(t.id, t));\n } else {\n this.tours.set(input.id, input);\n }\n }\n\n /* ── Queries ──────────────────────────────────── */\n\n /** Return a registered tour by ID. */\n getTour(id: string): TourDefinition | undefined {\n return this.tours.get(id);\n }\n\n /** Return all registered tours. */\n getAvailableTours(): TourDefinition[] {\n return Array.from(this.tours.values());\n }\n\n /**\n * Whether a `first-visit` tour should auto-start.\n * Returns `false` if the tour is manual, already completed, or dismissed.\n */\n shouldAutoStart(tourId: string): boolean {\n const tour = this.tours.get(tourId);\n if (!tour || tour.trigger !== 'first-visit') return false;\n return (\n !this.persistedState.completed.includes(tourId) &&\n !this.persistedState.dismissed.includes(tourId)\n );\n }\n\n /** Whether any tour is currently active. */\n isActive(): boolean {\n return this.activeTourId !== null;\n }\n\n /* ── Tour control ─────────────────────────────── */\n\n /** Start a tour by ID. No-op if the tour doesn't exist or has no steps. */\n start(tourId: string): void {\n const tour = this.tours.get(tourId);\n if (!tour || tour.steps.length === 0) return;\n\n this.activeTourId = tourId;\n this.currentStepIndex = 0;\n this.notify();\n }\n\n /** Advance to the next step, or complete the tour if on the last step. */\n nextStep(): void {\n if (!this.activeTourId) return;\n const tour = this.tours.get(this.activeTourId)!;\n\n if (this.currentStepIndex < tour.steps.length - 1) {\n this.currentStepIndex++;\n this.notify();\n } else {\n this.completeTour();\n }\n }\n\n /** Go back to the previous step. No-op if already on step 0. */\n prevStep(): void {\n if (!this.activeTourId) return;\n if (this.currentStepIndex > 0) {\n this.currentStepIndex--;\n this.notify();\n }\n }\n\n /** Skip / dismiss the current tour. Persists \"dismissed\" state. */\n skipTour(): void {\n if (!this.activeTourId) return;\n const id = this.activeTourId;\n const tour = this.tours.get(id);\n\n if (!this.persistedState.dismissed.includes(id)) {\n this.persistedState.dismissed.push(id);\n this.saveState();\n }\n\n this.activeTourId = null;\n this.currentStepIndex = 0;\n this.notify();\n\n tour?.onSkip?.();\n }\n\n private completeTour(): void {\n if (!this.activeTourId) return;\n const id = this.activeTourId;\n const tour = this.tours.get(id);\n\n if (!this.persistedState.completed.includes(id)) {\n this.persistedState.completed.push(id);\n this.saveState();\n }\n\n this.activeTourId = null;\n this.currentStepIndex = 0;\n this.notify();\n\n tour?.onComplete?.();\n }\n\n /* ── Snapshot (current state for overlay) ─────── */\n\n /** Return a snapshot of the current tour state, or `null` if inactive. */\n getSnapshot(): TourSnapshot | null {\n if (!this.activeTourId) return null;\n const tour = this.tours.get(this.activeTourId);\n if (!tour) return null;\n\n const step = tour.steps[this.currentStepIndex];\n if (!step) return null;\n\n const targetElement = this.findTarget(step.target);\n const targetRect = targetElement?.getBoundingClientRect() ?? null;\n\n return {\n tourId: this.activeTourId,\n tourName: tour.name,\n step,\n stepIndex: this.currentStepIndex,\n totalSteps: tour.steps.length,\n targetRect,\n targetElement,\n };\n }\n\n /* ── Shadow DOM target resolution ─────────────── */\n\n /** Find a DOM element by its `data-tour-id` (or custom attribute). */\n findTarget(targetId: string): Element | null {\n return deepQuery(`[${this.targetAttribute}=\"${targetId}\"]`, document.body);\n }\n\n /* ── Observer pattern ─────────────────────────── */\n\n /** Subscribe to snapshot changes. Returns an unsubscribe function. */\n subscribe(listener: TourListener): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n private notify(): void {\n const snapshot = this.getSnapshot();\n this.listeners.forEach(listener => listener(snapshot));\n }\n\n /* ── Reset (for testing & demos) ──────────────── */\n\n /** Clear all persisted state and stop any active tour. */\n resetAll(): void {\n this.persistedState = { completed: [], dismissed: [] };\n this.activeTourId = null;\n this.currentStepIndex = 0;\n this.tours.clear();\n this.saveState();\n this.notify();\n }\n}\n\n// ── Factory ──────────────────────────────────────────────────────────────────\n\n/**\n * Create a new `TourService` instance.\n *\n * @example\n * ```ts\n * import { createTourService } from 'torchlit';\n *\n * const tours = createTourService({ storageKey: 'my-app-tours' });\n * tours.register([...]);\n * tours.start('onboarding');\n * ```\n */\nexport function createTourService(config?: TourConfig): TourService {\n return new TourService(config);\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import type { TourConfig, TourDefinition, TourListener, TourSnapshot } from './types.js';
2
+ export declare class TourService {
3
+ private tours;
4
+ private persistedState;
5
+ private activeTourId;
6
+ private currentStepIndex;
7
+ private listeners;
8
+ private readonly storageKey;
9
+ private readonly storage;
10
+ private readonly targetAttribute;
11
+ readonly spotlightPadding: number;
12
+ constructor(config?: TourConfig);
13
+ private loadState;
14
+ private saveState;
15
+ /** Register a single tour definition. */
16
+ register(tours: TourDefinition[]): void;
17
+ register(tour: TourDefinition): void;
18
+ /** Return a registered tour by ID. */
19
+ getTour(id: string): TourDefinition | undefined;
20
+ /** Return all registered tours. */
21
+ getAvailableTours(): TourDefinition[];
22
+ /**
23
+ * Whether a `first-visit` tour should auto-start.
24
+ * Returns `false` if the tour is manual, already completed, or dismissed.
25
+ */
26
+ shouldAutoStart(tourId: string): boolean;
27
+ /** Whether any tour is currently active. */
28
+ isActive(): boolean;
29
+ /** Start a tour by ID. No-op if the tour doesn't exist or has no steps. */
30
+ start(tourId: string): void;
31
+ /** Advance to the next step, or complete the tour if on the last step. */
32
+ nextStep(): void;
33
+ /** Go back to the previous step. No-op if already on step 0. */
34
+ prevStep(): void;
35
+ /** Skip / dismiss the current tour. Persists "dismissed" state. */
36
+ skipTour(): void;
37
+ private completeTour;
38
+ /** Return a snapshot of the current tour state, or `null` if inactive. */
39
+ getSnapshot(): TourSnapshot | null;
40
+ /** Find a DOM element by its `data-tour-id` (or custom attribute). */
41
+ findTarget(targetId: string): Element | null;
42
+ /** Subscribe to snapshot changes. Returns an unsubscribe function. */
43
+ subscribe(listener: TourListener): () => void;
44
+ private notify;
45
+ /** Clear all persisted state and stop any active tour. */
46
+ resetAll(): void;
47
+ }
48
+ /**
49
+ * Create a new `TourService` instance.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { createTourService } from 'torchlit';
54
+ *
55
+ * const tours = createTourService({ storageKey: 'my-app-tours' });
56
+ * tours.register([...]);
57
+ * tours.start('onboarding');
58
+ * ```
59
+ */
60
+ export declare function createTourService(config?: TourConfig): TourService;
61
+ //# sourceMappingURL=tour-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-service.d.ts","sourceRoot":"","sources":["../src/tour-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EAGb,MAAM,YAAY,CAAC;AA6BpB,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,cAAc,CAAY;IAClC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,SAAS,CAAgC;IAGjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;gBAEtB,MAAM,GAAE,UAAe;IAUnC,OAAO,CAAC,SAAS;IAgBjB,OAAO,CAAC,SAAS;IAUjB,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,IAAI;IACvC,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAWpC,sCAAsC;IACtC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAI/C,mCAAmC;IACnC,iBAAiB,IAAI,cAAc,EAAE;IAIrC;;;OAGG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IASxC,4CAA4C;IAC5C,QAAQ,IAAI,OAAO;IAMnB,2EAA2E;IAC3E,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAS3B,0EAA0E;IAC1E,QAAQ,IAAI,IAAI;IAYhB,gEAAgE;IAChE,QAAQ,IAAI,IAAI;IAQhB,mEAAmE;IACnE,QAAQ,IAAI,IAAI;IAiBhB,OAAO,CAAC,YAAY;IAmBpB,0EAA0E;IAC1E,WAAW,IAAI,YAAY,GAAG,IAAI;IAwBlC,sEAAsE;IACtE,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAM5C,sEAAsE;IACtE,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;IAK7C,OAAO,CAAC,MAAM;IAOd,0DAA0D;IAC1D,QAAQ,IAAI,IAAI;CAQjB;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,WAAW,CAElE"}
@@ -0,0 +1,6 @@
1
+ import { T, c } from "./tour-service-BKz7eeWb.js";
2
+ export {
3
+ T as TourService,
4
+ c as createTourService
5
+ };
6
+ //# sourceMappingURL=tour-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-service.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,92 @@
1
+ /** Where to position the tooltip relative to the spotlight target. */
2
+ export type TourPlacement = 'top' | 'bottom' | 'left' | 'right';
3
+ export interface TourStep {
4
+ /**
5
+ * Matches `[data-tour-id="..."]` on the target element.
6
+ * Deep shadow DOM traversal is used automatically.
7
+ * Use `'_none_'` for a centered "welcome" card with no spotlight.
8
+ */
9
+ target: string;
10
+ /** Bold title shown in the tooltip. */
11
+ title: string;
12
+ /** Descriptive message shown below the title. */
13
+ message: string;
14
+ /** Where to position the tooltip relative to the target. */
15
+ placement: TourPlacement;
16
+ /**
17
+ * Arbitrary route / view hint.
18
+ * When set, a `tour-route-change` event is dispatched with `{ route }` detail
19
+ * so the host application can switch views before the step renders.
20
+ */
21
+ route?: string;
22
+ /**
23
+ * Optional async hook that runs **before** the step is shown.
24
+ * Use this for route navigation, data loading, or any async prep work.
25
+ */
26
+ beforeShow?: () => void | Promise<void>;
27
+ }
28
+ export interface TourDefinition {
29
+ /** Unique tour identifier. */
30
+ id: string;
31
+ /** Human-readable tour name. */
32
+ name: string;
33
+ /**
34
+ * `'first-visit'` — auto-triggers on first page load (unless completed/dismissed).
35
+ * `'manual'` — only starts when explicitly called via `service.start(id)`.
36
+ */
37
+ trigger: 'first-visit' | 'manual';
38
+ /** Ordered list of tour steps. */
39
+ steps: TourStep[];
40
+ /** Called when the user completes every step in the tour. */
41
+ onComplete?: () => void;
42
+ /** Called when the user skips / dismisses the tour. */
43
+ onSkip?: () => void;
44
+ }
45
+ export interface TourState {
46
+ /** Tour IDs that have been completed (user went through all steps). */
47
+ completed: string[];
48
+ /** Tour IDs that have been dismissed (user skipped). */
49
+ dismissed: string[];
50
+ }
51
+ export interface TourSnapshot {
52
+ tourId: string;
53
+ tourName: string;
54
+ step: TourStep;
55
+ stepIndex: number;
56
+ totalSteps: number;
57
+ targetRect: DOMRect | null;
58
+ targetElement: Element | null;
59
+ }
60
+ /**
61
+ * Minimal storage interface.
62
+ * Defaults to `localStorage` when not provided.
63
+ */
64
+ export interface StorageAdapter {
65
+ getItem(key: string): string | null;
66
+ setItem(key: string, value: string): void;
67
+ }
68
+ export interface TourConfig {
69
+ /**
70
+ * Key used for persisting tour state.
71
+ * @default `'torchlit-state'`
72
+ */
73
+ storageKey?: string;
74
+ /**
75
+ * Custom storage adapter. Useful for SSR, sessionStorage,
76
+ * or API-backed persistence.
77
+ * @default localStorage
78
+ */
79
+ storage?: StorageAdapter;
80
+ /**
81
+ * The `data-*` attribute used to locate tour targets.
82
+ * @default `'data-tour-id'`
83
+ */
84
+ targetAttribute?: string;
85
+ /**
86
+ * Padding (in px) around the spotlight cutout.
87
+ * @default 10
88
+ */
89
+ spotlightPadding?: number;
90
+ }
91
+ export type TourListener = (snapshot: TourSnapshot | null) => void;
92
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAIhE,MAAM,WAAW,QAAQ;IACvB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IAEd,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAEhB,4DAA4D;IAC5D,SAAS,EAAE,aAAa,CAAC;IAEzB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAID,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IAEX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,EAAE,aAAa,GAAG,QAAQ,CAAC;IAElC,kCAAkC;IAClC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAElB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IAExB,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAID,MAAM,WAAW,SAAS;IACxB,uEAAuE;IACvE,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB,wDAAwD;IACxD,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAID,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B;AAID;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAID,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Recursively search the DOM — including shadow roots — for an element
3
+ * matching the given CSS selector.
4
+ *
5
+ * This is the key differentiator vs. libraries like Shepherd.js or Intro.js
6
+ * which cannot pierce shadow DOM boundaries.
7
+ *
8
+ * @param selector A valid CSS selector string.
9
+ * @param root The root element (or Document) to start searching from.
10
+ * Defaults to `document.body`.
11
+ * @returns The first matching `Element`, or `null`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { deepQuery } from 'torchlit';
16
+ *
17
+ * const el = deepQuery('[data-tour-id="sidebar-nav"]');
18
+ * ```
19
+ */
20
+ export declare function deepQuery(selector: string, root?: Element | Document): Element | null;
21
+ //# sourceMappingURL=deep-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-query.d.ts","sourceRoot":"","sources":["../../src/utils/deep-query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,OAAO,GAAG,QAAwB,GACvC,OAAO,GAAG,IAAI,CAehB"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "torchlit",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight guided-tour & onboarding library built on Lit web components. Shadow DOM aware, framework-agnostic, tiny footprint.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./service": {
15
+ "import": "./dist/tour-service.js",
16
+ "types": "./dist/tour-service.d.ts"
17
+ },
18
+ "./overlay": {
19
+ "import": "./dist/tour-overlay.js",
20
+ "types": "./dist/tour-overlay.d.ts"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "sideEffects": [
27
+ "./dist/tour-overlay.js"
28
+ ],
29
+ "scripts": {
30
+ "dev": "vite serve examples",
31
+ "build": "vite build && tsc --emitDeclarationOnly --outDir dist",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "preview": "vite preview",
35
+ "prepublishOnly": "npm test && npm run build"
36
+ },
37
+ "keywords": [
38
+ "tour",
39
+ "onboarding",
40
+ "torchlit",
41
+ "spotlight",
42
+ "guided-tour",
43
+ "web-components",
44
+ "lit",
45
+ "shadow-dom",
46
+ "walkthrough",
47
+ "tooltip"
48
+ ],
49
+ "author": "Barry King (https://github.com/barryking)",
50
+ "license": "MIT",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/barryking/torchlit.git"
54
+ },
55
+ "homepage": "https://github.com/barryking/torchlit",
56
+ "bugs": "https://github.com/barryking/torchlit/issues",
57
+ "peerDependencies": {
58
+ "lit": "^3.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "jsdom": "^28.0.0",
62
+ "lit": "^3.2.1",
63
+ "typescript": "^5.7.0",
64
+ "vite": "^6.1.0",
65
+ "vitest": "^3.0.0"
66
+ }
67
+ }