torchlit 0.2.2 → 0.3.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.
- package/README.md +19 -8
- package/dist/core/tour-service.d.ts +32 -0
- package/dist/core/tour-service.d.ts.map +1 -0
- package/dist/core/types.d.ts +32 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/dom/deep-query.d.ts +2 -0
- package/dist/dom/deep-query.d.ts.map +1 -0
- package/dist/dom/positioning.d.ts +31 -0
- package/dist/dom/positioning.d.ts.map +1 -0
- package/dist/dom/scroll-manager.d.ts +4 -0
- package/dist/dom/scroll-manager.d.ts.map +1 -0
- package/dist/dom/target-resolver.d.ts +9 -0
- package/dist/dom/target-resolver.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/overlay/focus-manager.d.ts +8 -0
- package/dist/overlay/focus-manager.d.ts.map +1 -0
- package/dist/overlay/step-runner.d.ts +22 -0
- package/dist/overlay/step-runner.d.ts.map +1 -0
- package/dist/overlay/types.d.ts +8 -0
- package/dist/overlay/types.d.ts.map +1 -0
- package/dist/tour-overlay-CBkFKv12.js +1056 -0
- package/dist/tour-overlay-CBkFKv12.js.map +1 -0
- package/dist/tour-overlay.d.ts +11 -27
- package/dist/tour-overlay.d.ts.map +1 -1
- package/dist/tour-overlay.js +5 -948
- package/dist/tour-overlay.js.map +1 -1
- package/dist/tour-service.d.ts +2 -60
- package/dist/tour-service.d.ts.map +1 -1
- package/dist/tour-service.js +19 -48
- package/dist/tour-service.js.map +1 -1
- package/dist/types.d.ts +1 -105
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/deep-query.d.ts +1 -20
- package/dist/utils/deep-query.d.ts.map +1 -1
- package/package.json +11 -3
- package/dist/deep-query-vkmcq1Dw.js +0 -16
- package/dist/deep-query-vkmcq1Dw.js.map +0 -1
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
import { css, LitElement, html, nothing } from "lit";
|
|
2
|
+
import { property, state, customElement } from "lit/decorators.js";
|
|
3
|
+
import { keyed } from "lit/directives/keyed.js";
|
|
4
|
+
const TOOLTIP_W = 320;
|
|
5
|
+
const TOOLTIP_H_MAX = 270;
|
|
6
|
+
const GAP = 16;
|
|
7
|
+
const VIEWPORT_MARGIN = 24;
|
|
8
|
+
const TOOLTIP_VERTICAL_OFFSET = 80;
|
|
9
|
+
function fitsInViewport(rect, viewportHeight = window.innerHeight) {
|
|
10
|
+
return rect.height + TOOLTIP_H_MAX + GAP * 2 < viewportHeight;
|
|
11
|
+
}
|
|
12
|
+
function bestPlacement(rect, preferred, spotlightPadding, viewport = { width: window.innerWidth, height: window.innerHeight }) {
|
|
13
|
+
const fits = (placement) => {
|
|
14
|
+
switch (placement) {
|
|
15
|
+
case "bottom":
|
|
16
|
+
return rect.bottom + spotlightPadding + GAP + TOOLTIP_H_MAX < viewport.height;
|
|
17
|
+
case "top":
|
|
18
|
+
return rect.top - spotlightPadding - GAP - TOOLTIP_H_MAX > 0;
|
|
19
|
+
case "right":
|
|
20
|
+
return rect.right + spotlightPadding + GAP + TOOLTIP_W < viewport.width;
|
|
21
|
+
case "left":
|
|
22
|
+
return rect.left - spotlightPadding - GAP - TOOLTIP_W > 0;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const opposite = {
|
|
26
|
+
top: "bottom",
|
|
27
|
+
bottom: "top",
|
|
28
|
+
left: "right",
|
|
29
|
+
right: "left"
|
|
30
|
+
};
|
|
31
|
+
const perpendicular = {
|
|
32
|
+
top: ["left", "right"],
|
|
33
|
+
bottom: ["left", "right"],
|
|
34
|
+
left: ["top", "bottom"],
|
|
35
|
+
right: ["top", "bottom"]
|
|
36
|
+
};
|
|
37
|
+
if (fits(preferred)) return preferred;
|
|
38
|
+
if (fits(opposite[preferred])) return opposite[preferred];
|
|
39
|
+
for (const placement of perpendicular[preferred]) {
|
|
40
|
+
if (fits(placement)) return placement;
|
|
41
|
+
}
|
|
42
|
+
return preferred;
|
|
43
|
+
}
|
|
44
|
+
function getTooltipPosition(rect, placement, spotlightPadding, viewportHeight = window.innerHeight) {
|
|
45
|
+
const visibleTop = Math.max(0, rect.top);
|
|
46
|
+
const visibleBottom = Math.min(viewportHeight, rect.bottom);
|
|
47
|
+
const visibleCenterY = (visibleTop + visibleBottom) / 2;
|
|
48
|
+
switch (placement) {
|
|
49
|
+
case "right":
|
|
50
|
+
return {
|
|
51
|
+
top: visibleCenterY - TOOLTIP_VERTICAL_OFFSET,
|
|
52
|
+
left: rect.right + spotlightPadding + GAP
|
|
53
|
+
};
|
|
54
|
+
case "left":
|
|
55
|
+
return {
|
|
56
|
+
top: visibleCenterY - TOOLTIP_VERTICAL_OFFSET,
|
|
57
|
+
left: rect.left - spotlightPadding - GAP - TOOLTIP_W
|
|
58
|
+
};
|
|
59
|
+
case "bottom":
|
|
60
|
+
return {
|
|
61
|
+
top: rect.bottom + spotlightPadding + GAP,
|
|
62
|
+
left: rect.left + rect.width / 2 - TOOLTIP_W / 2
|
|
63
|
+
};
|
|
64
|
+
case "top":
|
|
65
|
+
return {
|
|
66
|
+
top: rect.top - spotlightPadding - GAP,
|
|
67
|
+
left: rect.left + rect.width / 2 - TOOLTIP_W / 2
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function clampToViewport(pos, viewport = { width: window.innerWidth, height: window.innerHeight }) {
|
|
72
|
+
return {
|
|
73
|
+
top: Math.max(
|
|
74
|
+
VIEWPORT_MARGIN,
|
|
75
|
+
Math.min(pos.top, viewport.height - TOOLTIP_H_MAX - VIEWPORT_MARGIN)
|
|
76
|
+
),
|
|
77
|
+
left: Math.max(
|
|
78
|
+
VIEWPORT_MARGIN,
|
|
79
|
+
Math.min(pos.left, viewport.width - TOOLTIP_W - VIEWPORT_MARGIN)
|
|
80
|
+
)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function getArrowClass(placement) {
|
|
84
|
+
switch (placement) {
|
|
85
|
+
case "right":
|
|
86
|
+
return "arrow-right";
|
|
87
|
+
case "left":
|
|
88
|
+
return "arrow-left";
|
|
89
|
+
case "bottom":
|
|
90
|
+
return "arrow-bottom";
|
|
91
|
+
case "top":
|
|
92
|
+
return "arrow-top";
|
|
93
|
+
default:
|
|
94
|
+
return "arrow-bottom";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function getArrowOffset(targetRect, tooltipPos, placement, viewportHeight = window.innerHeight) {
|
|
98
|
+
const arrowSize = 12;
|
|
99
|
+
const minOffset = arrowSize + 8;
|
|
100
|
+
if (placement === "top" || placement === "bottom") {
|
|
101
|
+
const targetCenterX = targetRect.left + targetRect.width / 2;
|
|
102
|
+
const offset2 = targetCenterX - tooltipPos.left;
|
|
103
|
+
const clamped2 = Math.max(minOffset, Math.min(offset2, TOOLTIP_W - minOffset));
|
|
104
|
+
return `${clamped2}px`;
|
|
105
|
+
}
|
|
106
|
+
const visibleTop = Math.max(0, targetRect.top);
|
|
107
|
+
const visibleBottom = Math.min(viewportHeight, targetRect.bottom);
|
|
108
|
+
const targetCenterY = (visibleTop + visibleBottom) / 2;
|
|
109
|
+
const offset = targetCenterY - tooltipPos.top;
|
|
110
|
+
const clamped = Math.max(minOffset, Math.min(offset, TOOLTIP_H_MAX - minOffset));
|
|
111
|
+
return `${clamped}px`;
|
|
112
|
+
}
|
|
113
|
+
function restoreScrollPosition(mode, savedScrollY) {
|
|
114
|
+
if (mode === "restore") {
|
|
115
|
+
window.scrollTo({ top: savedScrollY, behavior: "smooth" });
|
|
116
|
+
} else if (mode === "top") {
|
|
117
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function scrollAndSettle(element, placement, spotlightPadding) {
|
|
121
|
+
const initialRect = element.getBoundingClientRect();
|
|
122
|
+
const viewportHeight = window.innerHeight;
|
|
123
|
+
if (fitsInViewport(initialRect, viewportHeight)) {
|
|
124
|
+
element.scrollIntoView({
|
|
125
|
+
behavior: "smooth",
|
|
126
|
+
block: "center",
|
|
127
|
+
inline: "nearest"
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
const desiredTop = placement === "top" ? TOOLTIP_H_MAX + GAP + spotlightPadding : viewportHeight * 0.15;
|
|
131
|
+
const scrollTarget = window.scrollY + initialRect.top - desiredTop;
|
|
132
|
+
window.scrollTo({ top: Math.max(0, scrollTarget), behavior: "smooth" });
|
|
133
|
+
}
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
let lastTop = element.getBoundingClientRect().top;
|
|
136
|
+
let stableFrames = 0;
|
|
137
|
+
let rafId = 0;
|
|
138
|
+
const maxWait = setTimeout(() => {
|
|
139
|
+
cancelAnimationFrame(rafId);
|
|
140
|
+
resolve();
|
|
141
|
+
}, 1500);
|
|
142
|
+
const poll = () => {
|
|
143
|
+
const top = element.getBoundingClientRect().top;
|
|
144
|
+
if (Math.abs(top - lastTop) < 1) {
|
|
145
|
+
stableFrames += 1;
|
|
146
|
+
} else {
|
|
147
|
+
stableFrames = 0;
|
|
148
|
+
}
|
|
149
|
+
lastTop = top;
|
|
150
|
+
if (stableFrames >= 3) {
|
|
151
|
+
clearTimeout(maxWait);
|
|
152
|
+
resolve();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
rafId = requestAnimationFrame(poll);
|
|
156
|
+
};
|
|
157
|
+
rafId = requestAnimationFrame(poll);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
class FocusManager {
|
|
161
|
+
constructor() {
|
|
162
|
+
this.previouslyFocused = null;
|
|
163
|
+
}
|
|
164
|
+
capture() {
|
|
165
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
166
|
+
this.previouslyFocused = document.activeElement;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
restore() {
|
|
170
|
+
this.previouslyFocused?.focus();
|
|
171
|
+
this.previouslyFocused = null;
|
|
172
|
+
}
|
|
173
|
+
focusDialog(root) {
|
|
174
|
+
root?.querySelector(".tour-tooltip, .tour-center-card")?.focus();
|
|
175
|
+
}
|
|
176
|
+
trapFocus(event, root) {
|
|
177
|
+
const container = root?.querySelector(
|
|
178
|
+
".tour-tooltip, .tour-center-card"
|
|
179
|
+
);
|
|
180
|
+
if (!container) return;
|
|
181
|
+
const focusable = container.querySelectorAll(
|
|
182
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
183
|
+
);
|
|
184
|
+
if (focusable.length === 0) return;
|
|
185
|
+
const first = focusable[0];
|
|
186
|
+
const last = focusable[focusable.length - 1];
|
|
187
|
+
const activeElement = root?.activeElement;
|
|
188
|
+
if (event.shiftKey) {
|
|
189
|
+
if (activeElement === first) {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
last.focus();
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (activeElement === last) {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
first.focus();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function deepQuery(selector, root = document.body) {
|
|
202
|
+
const found = root.querySelector(selector);
|
|
203
|
+
if (found) return found;
|
|
204
|
+
const children = root.querySelectorAll("*");
|
|
205
|
+
for (const element of children) {
|
|
206
|
+
if (element.shadowRoot) {
|
|
207
|
+
const shadowResult = deepQuery(selector, element.shadowRoot);
|
|
208
|
+
if (shadowResult) return shadowResult;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const DEFAULT_TARGET_ATTR = "data-tour-id";
|
|
214
|
+
const DEFAULT_TIMEOUT = 3e3;
|
|
215
|
+
function resolveTargetSelector(targetId, targetAttribute = DEFAULT_TARGET_ATTR) {
|
|
216
|
+
return `[${targetAttribute}="${targetId}"]`;
|
|
217
|
+
}
|
|
218
|
+
function resolveTarget(targetId, targetAttribute = DEFAULT_TARGET_ATTR, root = document.body) {
|
|
219
|
+
if (!targetId || targetId === "_none_") return null;
|
|
220
|
+
return deepQuery(resolveTargetSelector(targetId, targetAttribute), root);
|
|
221
|
+
}
|
|
222
|
+
async function waitForTarget(targetId, targetAttribute = DEFAULT_TARGET_ATTR, timeout = DEFAULT_TIMEOUT) {
|
|
223
|
+
const existing = resolveTarget(targetId, targetAttribute);
|
|
224
|
+
if (existing) return existing;
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
let resolved = false;
|
|
227
|
+
const observer = new MutationObserver(() => {
|
|
228
|
+
const element = resolveTarget(targetId, targetAttribute);
|
|
229
|
+
if (!element) return;
|
|
230
|
+
resolved = true;
|
|
231
|
+
observer.disconnect();
|
|
232
|
+
resolve(element);
|
|
233
|
+
});
|
|
234
|
+
observer.observe(document.body, {
|
|
235
|
+
childList: true,
|
|
236
|
+
subtree: true
|
|
237
|
+
});
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
if (resolved) return;
|
|
240
|
+
observer.disconnect();
|
|
241
|
+
resolve(resolveTarget(targetId, targetAttribute));
|
|
242
|
+
}, timeout);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function resolveStepTarget(step, targetAttribute = DEFAULT_TARGET_ATTR) {
|
|
246
|
+
const targetElement = resolveTarget(step.target, targetAttribute);
|
|
247
|
+
return {
|
|
248
|
+
targetElement,
|
|
249
|
+
targetRect: targetElement?.getBoundingClientRect() ?? null
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const TARGET_CONTEXT_MARGIN = 32;
|
|
253
|
+
class StepRunner {
|
|
254
|
+
constructor(options) {
|
|
255
|
+
this.options = options;
|
|
256
|
+
this.autoAdvanceTimer = null;
|
|
257
|
+
}
|
|
258
|
+
clearAutoAdvance() {
|
|
259
|
+
if (this.autoAdvanceTimer !== null) {
|
|
260
|
+
clearTimeout(this.autoAdvanceTimer);
|
|
261
|
+
this.autoAdvanceTimer = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
startAutoAdvance(ms) {
|
|
265
|
+
this.clearAutoAdvance();
|
|
266
|
+
this.autoAdvanceTimer = setTimeout(() => {
|
|
267
|
+
this.autoAdvanceTimer = null;
|
|
268
|
+
this.options.nextStep();
|
|
269
|
+
}, ms);
|
|
270
|
+
}
|
|
271
|
+
async prepareStep(snapshot) {
|
|
272
|
+
if (snapshot.step.beforeShow) {
|
|
273
|
+
try {
|
|
274
|
+
await snapshot.step.beforeShow();
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error("[torchlit] beforeShow hook failed:", error);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (snapshot.step.route) {
|
|
280
|
+
this.options.dispatchRouteChange(snapshot.step.route);
|
|
281
|
+
}
|
|
282
|
+
if (snapshot.step.target && snapshot.step.target !== "_none_") {
|
|
283
|
+
await waitForTarget(snapshot.step.target, this.options.targetAttribute);
|
|
284
|
+
}
|
|
285
|
+
const currentSnapshot = this.options.getCurrentSnapshot() ?? snapshot;
|
|
286
|
+
const tour = this.options.getTour(currentSnapshot.tourId);
|
|
287
|
+
if (!tour) return null;
|
|
288
|
+
let resolved = this.resolveSnapshot(currentSnapshot, tour);
|
|
289
|
+
if (resolved.targetElement && this.shouldScrollIntoView(resolved)) {
|
|
290
|
+
await scrollAndSettle(
|
|
291
|
+
resolved.targetElement,
|
|
292
|
+
resolved.step.placement,
|
|
293
|
+
this.options.spotlightPadding
|
|
294
|
+
);
|
|
295
|
+
resolved = this.resolveSnapshot(
|
|
296
|
+
this.options.getCurrentSnapshot() ?? currentSnapshot,
|
|
297
|
+
tour
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return resolved;
|
|
301
|
+
}
|
|
302
|
+
resolveSnapshot(snapshot, tour) {
|
|
303
|
+
const { targetElement, targetRect } = resolveStepTarget(
|
|
304
|
+
snapshot.step,
|
|
305
|
+
this.options.targetAttribute
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
...snapshot,
|
|
309
|
+
tour,
|
|
310
|
+
targetElement,
|
|
311
|
+
targetRect
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
shouldScrollIntoView(snapshot) {
|
|
315
|
+
const rect = snapshot.targetRect;
|
|
316
|
+
if (!rect) return false;
|
|
317
|
+
const viewportHeight = window.innerHeight;
|
|
318
|
+
const fits = rect.height + TOOLTIP_H_MAX + TARGET_CONTEXT_MARGIN < viewportHeight;
|
|
319
|
+
const inView = fits ? rect.top >= 0 && rect.bottom <= viewportHeight && rect.left >= 0 && rect.right <= window.innerWidth : snapshot.step.placement === "top" ? rect.top >= TOOLTIP_H_MAX + GAP + this.options.spotlightPadding && rect.top < viewportHeight : rect.top >= 0 && rect.top < viewportHeight;
|
|
320
|
+
return !inView;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
var __defProp = Object.defineProperty;
|
|
324
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
325
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
326
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
327
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
328
|
+
if (decorator = decorators[i])
|
|
329
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
330
|
+
if (kind && result) __defProp(target, key, result);
|
|
331
|
+
return result;
|
|
332
|
+
};
|
|
333
|
+
let TorchlitOverlay = class extends LitElement {
|
|
334
|
+
constructor() {
|
|
335
|
+
super(...arguments);
|
|
336
|
+
this.snapshot = null;
|
|
337
|
+
this.visible = false;
|
|
338
|
+
this.teardownTimer = null;
|
|
339
|
+
this.focusManager = new FocusManager();
|
|
340
|
+
this.stepRunner = null;
|
|
341
|
+
this.lastResolvedPlacement = "bottom";
|
|
342
|
+
this.scrollRafId = 0;
|
|
343
|
+
this.savedScrollY = 0;
|
|
344
|
+
this.activeTour = null;
|
|
345
|
+
this.resolvedTargetElement = null;
|
|
346
|
+
this.changeToken = 0;
|
|
347
|
+
this.handleResize = () => {
|
|
348
|
+
this.refreshSnapshotFromTarget();
|
|
349
|
+
};
|
|
350
|
+
this.handleScroll = () => {
|
|
351
|
+
if (!this.snapshot || this.scrollRafId) return;
|
|
352
|
+
this.scrollRafId = requestAnimationFrame(() => {
|
|
353
|
+
this.scrollRafId = 0;
|
|
354
|
+
this.refreshSnapshotFromTarget();
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
this.handleKeydown = (e) => {
|
|
358
|
+
if (!this.snapshot || !this.service) return;
|
|
359
|
+
if (e.key === "Escape") {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
this.clearAutoAdvance();
|
|
362
|
+
this.service.skipTour();
|
|
363
|
+
} else if (e.key === "ArrowRight" || e.key === "Enter") {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
this.clearAutoAdvance();
|
|
366
|
+
this.service.nextStep();
|
|
367
|
+
} else if (e.key === "ArrowLeft") {
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
this.clearAutoAdvance();
|
|
370
|
+
this.service.prevStep();
|
|
371
|
+
} else if (e.key === "Tab") {
|
|
372
|
+
this.focusManager.trapFocus(e, this.shadowRoot);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
this.handleBackdropClick = () => {
|
|
376
|
+
this.clearAutoAdvance();
|
|
377
|
+
this.service?.skipTour();
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/* ── Lifecycle ──────────────────────────────────── */
|
|
381
|
+
connectedCallback() {
|
|
382
|
+
super.connectedCallback();
|
|
383
|
+
if (this.service) {
|
|
384
|
+
this.attachService();
|
|
385
|
+
}
|
|
386
|
+
window.addEventListener("resize", this.handleResize);
|
|
387
|
+
window.addEventListener("scroll", this.handleScroll, true);
|
|
388
|
+
window.addEventListener("keydown", this.handleKeydown);
|
|
389
|
+
}
|
|
390
|
+
disconnectedCallback() {
|
|
391
|
+
super.disconnectedCallback();
|
|
392
|
+
this.unsubscribe?.();
|
|
393
|
+
this.clearAutoAdvance();
|
|
394
|
+
if (this.scrollRafId) cancelAnimationFrame(this.scrollRafId);
|
|
395
|
+
if (this.teardownTimer) clearTimeout(this.teardownTimer);
|
|
396
|
+
window.removeEventListener("resize", this.handleResize);
|
|
397
|
+
window.removeEventListener("scroll", this.handleScroll, true);
|
|
398
|
+
window.removeEventListener("keydown", this.handleKeydown);
|
|
399
|
+
}
|
|
400
|
+
updated(changed) {
|
|
401
|
+
if (changed.has("service") && this.service) {
|
|
402
|
+
this.unsubscribe?.();
|
|
403
|
+
this.attachService();
|
|
404
|
+
}
|
|
405
|
+
if (this.visible && this.snapshot) {
|
|
406
|
+
this.adjustTooltipPosition();
|
|
407
|
+
this.updateComplete.then(() => {
|
|
408
|
+
this.focusManager.focusDialog(this.shadowRoot);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* After rendering, measure the tooltip's actual height and correct
|
|
414
|
+
* its position for 'top' placement (the only one that depends on
|
|
415
|
+
* tooltip height). This eliminates hardcoded height estimates.
|
|
416
|
+
*/
|
|
417
|
+
adjustTooltipPosition() {
|
|
418
|
+
if (this.lastResolvedPlacement !== "top") return;
|
|
419
|
+
const tooltip = this.shadowRoot?.querySelector(".tour-tooltip");
|
|
420
|
+
const targetRect = this.snapshot?.targetRect;
|
|
421
|
+
if (!tooltip || !targetRect) return;
|
|
422
|
+
const PADDING = this.service?.spotlightPadding ?? 10;
|
|
423
|
+
const actualHeight = tooltip.getBoundingClientRect().height;
|
|
424
|
+
const correctTop = targetRect.top - PADDING - GAP - actualHeight;
|
|
425
|
+
const clampedTop = Math.max(VIEWPORT_MARGIN, correctTop);
|
|
426
|
+
tooltip.style.top = `${clampedTop}px`;
|
|
427
|
+
}
|
|
428
|
+
attachService() {
|
|
429
|
+
this.stepRunner = new StepRunner({
|
|
430
|
+
getCurrentSnapshot: () => this.service.getSnapshot(),
|
|
431
|
+
getTour: (tourId) => this.getTourDefinition(tourId),
|
|
432
|
+
nextStep: () => this.service.nextStep(),
|
|
433
|
+
spotlightPadding: this.service.spotlightPadding,
|
|
434
|
+
targetAttribute: this.service.targetAttribute,
|
|
435
|
+
dispatchRouteChange: (route) => this.dispatchRouteChange(route)
|
|
436
|
+
});
|
|
437
|
+
this.unsubscribe = this.service.subscribe((snapshot) => {
|
|
438
|
+
void this.handleTourChange(snapshot);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
clearAutoAdvance() {
|
|
442
|
+
this.stepRunner?.clearAutoAdvance();
|
|
443
|
+
}
|
|
444
|
+
startAutoAdvance(ms) {
|
|
445
|
+
this.stepRunner?.startAutoAdvance(ms);
|
|
446
|
+
}
|
|
447
|
+
async handleTourChange(snapshot) {
|
|
448
|
+
const token = ++this.changeToken;
|
|
449
|
+
this.clearAutoAdvance();
|
|
450
|
+
if (this.teardownTimer) {
|
|
451
|
+
clearTimeout(this.teardownTimer);
|
|
452
|
+
this.teardownTimer = null;
|
|
453
|
+
}
|
|
454
|
+
if (!snapshot) {
|
|
455
|
+
const endingTour = this.activeTour;
|
|
456
|
+
this.visible = false;
|
|
457
|
+
this.activeTour = null;
|
|
458
|
+
this.resolvedTargetElement = null;
|
|
459
|
+
this.teardownTimer = setTimeout(() => {
|
|
460
|
+
if (token !== this.changeToken) return;
|
|
461
|
+
this.snapshot = null;
|
|
462
|
+
this.focusManager.restore();
|
|
463
|
+
restoreScrollPosition(endingTour?.onEndScroll ?? "restore", this.savedScrollY);
|
|
464
|
+
}, 300);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const isNewTour = snapshot.tourId !== this.activeTour?.id;
|
|
468
|
+
if (!this.activeTour) {
|
|
469
|
+
this.focusManager.capture();
|
|
470
|
+
}
|
|
471
|
+
if (isNewTour) {
|
|
472
|
+
this.savedScrollY = window.scrollY;
|
|
473
|
+
}
|
|
474
|
+
const resolved = await this.stepRunner?.prepareStep(snapshot);
|
|
475
|
+
if (!resolved || token !== this.changeToken) return;
|
|
476
|
+
this.activeTour = resolved.tour;
|
|
477
|
+
this.snapshot = resolved;
|
|
478
|
+
this.resolvedTargetElement = resolved.targetElement;
|
|
479
|
+
requestAnimationFrame(() => {
|
|
480
|
+
if (token !== this.changeToken) return;
|
|
481
|
+
this.visible = true;
|
|
482
|
+
if (resolved.step.autoAdvance) {
|
|
483
|
+
this.startAutoAdvance(resolved.step.autoAdvance);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
dispatchRouteChange(route) {
|
|
488
|
+
this.dispatchEvent(
|
|
489
|
+
new CustomEvent("tour-route-change", {
|
|
490
|
+
detail: { route },
|
|
491
|
+
bubbles: true,
|
|
492
|
+
composed: true
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
getTourDefinition(tourId) {
|
|
497
|
+
return this.service?.getTour(tourId);
|
|
498
|
+
}
|
|
499
|
+
refreshSnapshotFromTarget() {
|
|
500
|
+
if (!this.snapshot) return;
|
|
501
|
+
const targetElement = this.resolvedTargetElement?.isConnected ? this.resolvedTargetElement : null;
|
|
502
|
+
this.snapshot = {
|
|
503
|
+
...this.snapshot,
|
|
504
|
+
targetElement,
|
|
505
|
+
targetRect: targetElement?.getBoundingClientRect() ?? null
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Determine the best placement for the tooltip, flipping when the preferred
|
|
510
|
+
* placement would clip the viewport. Tries: preferred → opposite → perpendicular.
|
|
511
|
+
*/
|
|
512
|
+
bestPlacement(rect, preferred) {
|
|
513
|
+
return bestPlacement(rect, preferred, this.service?.spotlightPadding ?? 10);
|
|
514
|
+
}
|
|
515
|
+
getTooltipPosition(rect, placement) {
|
|
516
|
+
return getTooltipPosition(rect, placement, this.service?.spotlightPadding ?? 10);
|
|
517
|
+
}
|
|
518
|
+
clampToViewport(pos) {
|
|
519
|
+
return clampToViewport(pos);
|
|
520
|
+
}
|
|
521
|
+
getArrowClass(placement) {
|
|
522
|
+
return getArrowClass(placement);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Compute the arrow's offset along the tooltip edge so it points at
|
|
526
|
+
* the center of the target element, clamped to stay within the tooltip.
|
|
527
|
+
*/
|
|
528
|
+
getArrowOffset(targetRect, tooltipPos, placement) {
|
|
529
|
+
return getArrowOffset(targetRect, tooltipPos, placement);
|
|
530
|
+
}
|
|
531
|
+
/* ── Render ─────────────────────────────────────── */
|
|
532
|
+
render() {
|
|
533
|
+
if (!this.snapshot) return html``;
|
|
534
|
+
const { step, stepIndex, totalSteps, targetRect } = this.snapshot;
|
|
535
|
+
if (!targetRect) {
|
|
536
|
+
return this.renderCenteredStep(step, stepIndex, totalSteps);
|
|
537
|
+
}
|
|
538
|
+
const PADDING = this.service?.spotlightPadding ?? 10;
|
|
539
|
+
const spotlightRadius = step.spotlightBorderRadius ? `border-radius: ${step.spotlightBorderRadius};` : "";
|
|
540
|
+
const spotlightStyle = `
|
|
541
|
+
top: ${targetRect.top - PADDING}px;
|
|
542
|
+
left: ${targetRect.left - PADDING}px;
|
|
543
|
+
width: ${targetRect.width + PADDING * 2}px;
|
|
544
|
+
height: ${targetRect.height + PADDING * 2}px;
|
|
545
|
+
${spotlightRadius}
|
|
546
|
+
`;
|
|
547
|
+
const resolved = this.bestPlacement(targetRect, step.placement);
|
|
548
|
+
this.lastResolvedPlacement = resolved;
|
|
549
|
+
const tooltipPos = this.clampToViewport(
|
|
550
|
+
this.getTooltipPosition(targetRect, resolved)
|
|
551
|
+
);
|
|
552
|
+
const arrowOffset = this.getArrowOffset(targetRect, tooltipPos, resolved);
|
|
553
|
+
const tooltipStyle = `top: ${tooltipPos.top}px; left: ${tooltipPos.left}px;`;
|
|
554
|
+
const stepLabel = `Step ${stepIndex + 1} of ${totalSteps}: ${step.title}`;
|
|
555
|
+
return html`
|
|
556
|
+
<!-- Screen reader announcement -->
|
|
557
|
+
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
558
|
+
${stepLabel}
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
<div
|
|
562
|
+
class="tour-backdrop ${this.visible ? "visible" : ""}"
|
|
563
|
+
part="backdrop"
|
|
564
|
+
@click=${this.handleBackdropClick}
|
|
565
|
+
></div>
|
|
566
|
+
|
|
567
|
+
<div class="tour-spotlight" part="spotlight" style=${spotlightStyle}></div>
|
|
568
|
+
|
|
569
|
+
<div
|
|
570
|
+
class="tour-tooltip ${this.visible ? "visible" : ""}"
|
|
571
|
+
part="tooltip"
|
|
572
|
+
style=${tooltipStyle}
|
|
573
|
+
role="dialog"
|
|
574
|
+
aria-modal="true"
|
|
575
|
+
aria-label="${step.title}"
|
|
576
|
+
aria-describedby="tour-desc"
|
|
577
|
+
tabindex="-1"
|
|
578
|
+
>
|
|
579
|
+
<div class="tour-arrow ${this.getArrowClass(resolved)}" style="--arrow-offset: ${arrowOffset}"></div>
|
|
580
|
+
|
|
581
|
+
<div class="tour-step-badge" aria-hidden="true">
|
|
582
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
583
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
584
|
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
|
585
|
+
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
|
586
|
+
</svg>
|
|
587
|
+
Step ${stepIndex + 1} of ${totalSteps}
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
<h3 class="tour-title">${step.title}</h3>
|
|
591
|
+
<div class="tour-message" id="tour-desc">${step.message}</div>
|
|
592
|
+
|
|
593
|
+
${this.renderProgressDots(stepIndex, totalSteps)}
|
|
594
|
+
${this.renderFooter(stepIndex, totalSteps)}
|
|
595
|
+
${this.renderAutoProgress(step, stepIndex)}
|
|
596
|
+
</div>
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
renderProgressDots(current, total) {
|
|
600
|
+
if (total <= 1) return nothing;
|
|
601
|
+
return html`
|
|
602
|
+
<div class="tour-progress" role="group" aria-label="Tour progress">
|
|
603
|
+
${Array.from({ length: total }, (_, i) => html`
|
|
604
|
+
<div
|
|
605
|
+
class="tour-dot ${i === current ? "active" : i < current ? "completed" : ""}"
|
|
606
|
+
role="presentation"
|
|
607
|
+
></div>
|
|
608
|
+
`)}
|
|
609
|
+
</div>
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
renderFooter(stepIndex, totalSteps, finishLabel = "Finish", finishAriaLabel = "Finish tour", showNavIcons = true) {
|
|
613
|
+
return html`
|
|
614
|
+
<div class="tour-footer">
|
|
615
|
+
<button
|
|
616
|
+
class="tour-skip"
|
|
617
|
+
aria-label="Skip tour"
|
|
618
|
+
@click=${() => {
|
|
619
|
+
this.clearAutoAdvance();
|
|
620
|
+
this.service.skipTour();
|
|
621
|
+
}}
|
|
622
|
+
>
|
|
623
|
+
Skip tour
|
|
624
|
+
</button>
|
|
625
|
+
<div class="tour-nav">
|
|
626
|
+
${stepIndex > 0 ? html`
|
|
627
|
+
<button
|
|
628
|
+
class="tour-btn"
|
|
629
|
+
aria-label="Go to previous step"
|
|
630
|
+
@click=${() => {
|
|
631
|
+
this.clearAutoAdvance();
|
|
632
|
+
this.service.prevStep();
|
|
633
|
+
}}
|
|
634
|
+
>
|
|
635
|
+
${showNavIcons ? html`
|
|
636
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
637
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
638
|
+
</svg>
|
|
639
|
+
` : nothing}
|
|
640
|
+
Back
|
|
641
|
+
</button>
|
|
642
|
+
` : nothing}
|
|
643
|
+
<button
|
|
644
|
+
class="tour-btn primary"
|
|
645
|
+
aria-label="${stepIndex === totalSteps - 1 ? finishAriaLabel : "Go to next step"}"
|
|
646
|
+
@click=${() => {
|
|
647
|
+
this.clearAutoAdvance();
|
|
648
|
+
this.service.nextStep();
|
|
649
|
+
}}
|
|
650
|
+
>
|
|
651
|
+
${stepIndex === totalSteps - 1 ? finishLabel : "Next"}
|
|
652
|
+
${stepIndex < totalSteps - 1 ? html`
|
|
653
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
654
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
655
|
+
</svg>
|
|
656
|
+
` : showNavIcons ? html`
|
|
657
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
658
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
659
|
+
</svg>
|
|
660
|
+
` : nothing}
|
|
661
|
+
</button>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
renderAutoProgress(step, stepIndex) {
|
|
667
|
+
if (!step.autoAdvance) return nothing;
|
|
668
|
+
return keyed(stepIndex, html`
|
|
669
|
+
<div
|
|
670
|
+
class="tour-auto-progress"
|
|
671
|
+
style="animation: autoAdvanceFill ${step.autoAdvance}ms linear forwards;"
|
|
672
|
+
aria-hidden="true"
|
|
673
|
+
></div>
|
|
674
|
+
`);
|
|
675
|
+
}
|
|
676
|
+
renderCenteredStep(step, stepIndex, totalSteps) {
|
|
677
|
+
const stepLabel = `Step ${stepIndex + 1} of ${totalSteps}: ${step.title}`;
|
|
678
|
+
return html`
|
|
679
|
+
<!-- Screen reader announcement -->
|
|
680
|
+
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
681
|
+
${stepLabel}
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<div
|
|
685
|
+
class="tour-backdrop ${this.visible ? "visible" : ""}"
|
|
686
|
+
part="backdrop"
|
|
687
|
+
@click=${this.handleBackdropClick}
|
|
688
|
+
></div>
|
|
689
|
+
|
|
690
|
+
<div
|
|
691
|
+
class="tour-center-card ${this.visible ? "visible" : ""}"
|
|
692
|
+
part="center-card"
|
|
693
|
+
role="dialog"
|
|
694
|
+
aria-modal="true"
|
|
695
|
+
aria-label="${step.title}"
|
|
696
|
+
aria-describedby="tour-desc-center"
|
|
697
|
+
tabindex="-1"
|
|
698
|
+
>
|
|
699
|
+
<div class="tour-center-icon" aria-hidden="true">
|
|
700
|
+
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
701
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
702
|
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
|
703
|
+
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
|
704
|
+
</svg>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
<h3 class="tour-title">${step.title}</h3>
|
|
708
|
+
<div class="tour-message" id="tour-desc-center">${step.message}</div>
|
|
709
|
+
|
|
710
|
+
${this.renderProgressDots(stepIndex, totalSteps)}
|
|
711
|
+
${this.renderFooter(stepIndex, totalSteps, "Let's go!", "Start the tour", false)}
|
|
712
|
+
${this.renderAutoProgress(step, stepIndex)}
|
|
713
|
+
</div>
|
|
714
|
+
`;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
TorchlitOverlay.styles = css`
|
|
718
|
+
:host {
|
|
719
|
+
display: block;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/* ── Visually hidden (sr-only) ─────────────────── */
|
|
723
|
+
|
|
724
|
+
.sr-only {
|
|
725
|
+
position: absolute;
|
|
726
|
+
width: 1px;
|
|
727
|
+
height: 1px;
|
|
728
|
+
padding: 0;
|
|
729
|
+
margin: -1px;
|
|
730
|
+
overflow: hidden;
|
|
731
|
+
clip: rect(0, 0, 0, 0);
|
|
732
|
+
white-space: nowrap;
|
|
733
|
+
border: 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/* ── Backdrop ──────────────────────────────────── */
|
|
737
|
+
|
|
738
|
+
.tour-backdrop {
|
|
739
|
+
position: fixed;
|
|
740
|
+
inset: 0;
|
|
741
|
+
z-index: 9998;
|
|
742
|
+
pointer-events: auto;
|
|
743
|
+
opacity: 0;
|
|
744
|
+
transition: opacity 0.3s ease;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.tour-backdrop.visible {
|
|
748
|
+
opacity: 1;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/* ── Spotlight (box-shadow cutout) ─────────────── */
|
|
752
|
+
|
|
753
|
+
.tour-spotlight {
|
|
754
|
+
position: fixed;
|
|
755
|
+
z-index: 9999;
|
|
756
|
+
border-radius: var(--tour-spotlight-radius, var(--radius-lg, 0.75rem));
|
|
757
|
+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
|
|
758
|
+
transition: top 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
759
|
+
left 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
760
|
+
width 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
761
|
+
height 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
762
|
+
pointer-events: none;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* Subtle pulsing ring around spotlight */
|
|
766
|
+
.tour-spotlight::after {
|
|
767
|
+
content: '';
|
|
768
|
+
position: absolute;
|
|
769
|
+
inset: -4px;
|
|
770
|
+
border-radius: inherit;
|
|
771
|
+
border: 2px solid var(--tour-primary, var(--primary, #F26122));
|
|
772
|
+
opacity: 0.5;
|
|
773
|
+
animation: spotlightPulse 2s ease-in-out infinite;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
@keyframes spotlightPulse {
|
|
777
|
+
0%, 100% { opacity: 0.3; transform: scale(1); }
|
|
778
|
+
50% { opacity: 0.7; transform: scale(1.01); }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/* ── Tooltip ───────────────────────────────────── */
|
|
782
|
+
|
|
783
|
+
.tour-tooltip {
|
|
784
|
+
position: fixed;
|
|
785
|
+
z-index: 10000;
|
|
786
|
+
box-sizing: border-box;
|
|
787
|
+
width: 320px;
|
|
788
|
+
background: var(--tour-card, var(--card, #fff));
|
|
789
|
+
border: 1px solid var(--tour-border, var(--border, #e5e5e5));
|
|
790
|
+
border-radius: var(--tour-tooltip-radius, var(--radius-lg, 0.75rem));
|
|
791
|
+
box-shadow: 0 20px 40px -8px rgba(0, 0, 0, 0.2),
|
|
792
|
+
0 8px 16px -4px rgba(0, 0, 0, 0.1);
|
|
793
|
+
padding: 1.25rem;
|
|
794
|
+
pointer-events: auto;
|
|
795
|
+
opacity: 0;
|
|
796
|
+
transform: translateY(8px) scale(0.96);
|
|
797
|
+
transition: opacity 0.25s ease, transform 0.25s ease,
|
|
798
|
+
top 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
799
|
+
left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.tour-tooltip:focus {
|
|
803
|
+
outline: none;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.tour-tooltip.visible {
|
|
807
|
+
opacity: 1;
|
|
808
|
+
transform: translateY(0) scale(1);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* Arrow — position along edge is set via inline --arrow-offset */
|
|
812
|
+
.tour-arrow {
|
|
813
|
+
position: absolute;
|
|
814
|
+
width: 12px;
|
|
815
|
+
height: 12px;
|
|
816
|
+
background: var(--tour-card, var(--card, #fff));
|
|
817
|
+
border: 1px solid var(--tour-border, var(--border, #e5e5e5));
|
|
818
|
+
transform: rotate(45deg);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* tooltip is above target → arrow at bottom of tooltip pointing down */
|
|
822
|
+
.tour-arrow.arrow-top {
|
|
823
|
+
bottom: -7px;
|
|
824
|
+
left: var(--arrow-offset, 50%);
|
|
825
|
+
margin-left: -6px;
|
|
826
|
+
border-top: none;
|
|
827
|
+
border-left: none;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/* tooltip is below target → arrow at top of tooltip pointing up */
|
|
831
|
+
.tour-arrow.arrow-bottom {
|
|
832
|
+
top: -7px;
|
|
833
|
+
left: var(--arrow-offset, 50%);
|
|
834
|
+
margin-left: -6px;
|
|
835
|
+
border-bottom: none;
|
|
836
|
+
border-right: none;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/* tooltip is right of target → arrow on left edge pointing left */
|
|
840
|
+
.tour-arrow.arrow-left {
|
|
841
|
+
right: -7px;
|
|
842
|
+
top: var(--arrow-offset, 50%);
|
|
843
|
+
margin-top: -6px;
|
|
844
|
+
border-bottom: none;
|
|
845
|
+
border-left: none;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/* tooltip is left of target → arrow on right edge pointing right */
|
|
849
|
+
.tour-arrow.arrow-right {
|
|
850
|
+
left: -7px;
|
|
851
|
+
top: var(--arrow-offset, 50%);
|
|
852
|
+
margin-top: -6px;
|
|
853
|
+
border-top: none;
|
|
854
|
+
border-right: none;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* ── Tooltip content ──────────────────────────── */
|
|
858
|
+
|
|
859
|
+
.tour-step-badge {
|
|
860
|
+
display: inline-flex;
|
|
861
|
+
align-items: center;
|
|
862
|
+
gap: 0.25rem;
|
|
863
|
+
font-size: 0.6875rem;
|
|
864
|
+
font-weight: 600;
|
|
865
|
+
text-transform: uppercase;
|
|
866
|
+
letter-spacing: 0.05em;
|
|
867
|
+
color: var(--tour-primary, var(--primary, #F26122));
|
|
868
|
+
margin-bottom: 0.5rem;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.tour-title {
|
|
872
|
+
margin: 0 0 0.375rem;
|
|
873
|
+
font-size: 1rem;
|
|
874
|
+
font-weight: 600;
|
|
875
|
+
color: var(--tour-foreground, var(--foreground, #1a1a1a));
|
|
876
|
+
line-height: 1.3;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.tour-message {
|
|
880
|
+
margin: 0 0 1rem;
|
|
881
|
+
font-size: 0.8125rem;
|
|
882
|
+
color: var(--tour-muted-foreground, var(--muted-foreground, #737373));
|
|
883
|
+
line-height: 1.55;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/* ── Progress dots ────────────────────────────── */
|
|
887
|
+
|
|
888
|
+
.tour-progress {
|
|
889
|
+
display: flex;
|
|
890
|
+
align-items: center;
|
|
891
|
+
gap: 0.375rem;
|
|
892
|
+
margin-bottom: 1rem;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.tour-dot {
|
|
896
|
+
width: 6px;
|
|
897
|
+
height: 6px;
|
|
898
|
+
border-radius: 50%;
|
|
899
|
+
background: var(--tour-muted, var(--muted, #e5e5e5));
|
|
900
|
+
transition: background 0.2s, transform 0.2s;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.tour-dot.active {
|
|
904
|
+
background: var(--tour-primary, var(--primary, #F26122));
|
|
905
|
+
transform: scale(1.3);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.tour-dot.completed {
|
|
909
|
+
background: var(--tour-primary, var(--primary, #F26122));
|
|
910
|
+
opacity: 0.5;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/* ── Auto-advance progress bar ────────────────── */
|
|
914
|
+
|
|
915
|
+
.tour-auto-progress {
|
|
916
|
+
position: absolute;
|
|
917
|
+
bottom: 0;
|
|
918
|
+
left: 0;
|
|
919
|
+
max-width: 100%;
|
|
920
|
+
height: 3px;
|
|
921
|
+
background: var(--tour-primary, var(--primary, #F26122));
|
|
922
|
+
opacity: 0.7;
|
|
923
|
+
border-radius: 0 0 var(--tour-tooltip-radius, var(--radius-lg, 0.75rem)) var(--tour-tooltip-radius, var(--radius-lg, 0.75rem));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
@keyframes autoAdvanceFill {
|
|
927
|
+
from { width: 0%; }
|
|
928
|
+
to { width: 100%; }
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/* ── Footer buttons ───────────────────────────── */
|
|
932
|
+
|
|
933
|
+
.tour-footer {
|
|
934
|
+
display: flex;
|
|
935
|
+
align-items: center;
|
|
936
|
+
justify-content: space-between;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.tour-skip {
|
|
940
|
+
font-size: 0.75rem;
|
|
941
|
+
color: var(--tour-muted-foreground, var(--muted-foreground, #737373));
|
|
942
|
+
background: none;
|
|
943
|
+
border: none;
|
|
944
|
+
cursor: pointer;
|
|
945
|
+
padding: 0.25rem 0;
|
|
946
|
+
transition: color 0.15s;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.tour-skip:hover {
|
|
950
|
+
color: var(--tour-foreground, var(--foreground, #1a1a1a));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.tour-nav {
|
|
954
|
+
display: flex;
|
|
955
|
+
gap: 0.5rem;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.tour-btn {
|
|
959
|
+
display: inline-flex;
|
|
960
|
+
align-items: center;
|
|
961
|
+
gap: 0.375rem;
|
|
962
|
+
padding: 0.4rem 0.875rem;
|
|
963
|
+
font-size: 0.8125rem;
|
|
964
|
+
font-weight: 500;
|
|
965
|
+
border-radius: var(--tour-btn-radius, var(--radius-md, 0.5rem));
|
|
966
|
+
border: 1px solid var(--tour-border, var(--border, #e5e5e5));
|
|
967
|
+
background: var(--tour-background, var(--background, #fff));
|
|
968
|
+
color: var(--tour-foreground, var(--foreground, #1a1a1a));
|
|
969
|
+
cursor: pointer;
|
|
970
|
+
transition: all 0.15s;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.tour-btn:hover {
|
|
974
|
+
background: var(--tour-muted, var(--muted, #f5f5f5));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.tour-btn:focus-visible {
|
|
978
|
+
outline: 2px solid var(--tour-primary, var(--primary, #F26122));
|
|
979
|
+
outline-offset: 2px;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.tour-btn.primary {
|
|
983
|
+
background: var(--tour-primary, var(--primary, #F26122));
|
|
984
|
+
color: var(--tour-primary-foreground, var(--primary-foreground, #fff));
|
|
985
|
+
border-color: var(--tour-primary, var(--primary, #F26122));
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.tour-btn.primary:hover {
|
|
989
|
+
opacity: 0.9;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.tour-btn svg {
|
|
993
|
+
width: 14px;
|
|
994
|
+
height: 14px;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* ── Welcome / no-target step ─────────────────── */
|
|
998
|
+
|
|
999
|
+
.tour-center-card {
|
|
1000
|
+
position: fixed;
|
|
1001
|
+
z-index: 10000;
|
|
1002
|
+
box-sizing: border-box;
|
|
1003
|
+
top: 50%;
|
|
1004
|
+
left: 50%;
|
|
1005
|
+
transform: translate(-50%, -50%) scale(0.96);
|
|
1006
|
+
width: 400px;
|
|
1007
|
+
max-width: calc(100vw - 2rem);
|
|
1008
|
+
background: var(--tour-card, var(--card, #fff));
|
|
1009
|
+
border: 1px solid var(--tour-border, var(--border, #e5e5e5));
|
|
1010
|
+
border-radius: var(--tour-card-radius, var(--radius-xl, 1rem));
|
|
1011
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
1012
|
+
padding: 2rem;
|
|
1013
|
+
text-align: center;
|
|
1014
|
+
pointer-events: auto;
|
|
1015
|
+
opacity: 0;
|
|
1016
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.tour-center-card:focus {
|
|
1020
|
+
outline: none;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.tour-center-card.visible {
|
|
1024
|
+
opacity: 1;
|
|
1025
|
+
transform: translate(-50%, -50%) scale(1);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.tour-center-icon {
|
|
1029
|
+
width: 48px;
|
|
1030
|
+
height: 48px;
|
|
1031
|
+
margin: 0 auto 1rem;
|
|
1032
|
+
background: var(--tour-primary, var(--primary, #F26122));
|
|
1033
|
+
border-radius: 50%;
|
|
1034
|
+
display: flex;
|
|
1035
|
+
align-items: center;
|
|
1036
|
+
justify-content: center;
|
|
1037
|
+
color: var(--tour-primary-foreground, var(--primary-foreground, #fff));
|
|
1038
|
+
}
|
|
1039
|
+
`;
|
|
1040
|
+
__decorateClass([
|
|
1041
|
+
property({ attribute: false })
|
|
1042
|
+
], TorchlitOverlay.prototype, "service", 2);
|
|
1043
|
+
__decorateClass([
|
|
1044
|
+
state()
|
|
1045
|
+
], TorchlitOverlay.prototype, "snapshot", 2);
|
|
1046
|
+
__decorateClass([
|
|
1047
|
+
state()
|
|
1048
|
+
], TorchlitOverlay.prototype, "visible", 2);
|
|
1049
|
+
TorchlitOverlay = __decorateClass([
|
|
1050
|
+
customElement("torchlit-overlay")
|
|
1051
|
+
], TorchlitOverlay);
|
|
1052
|
+
export {
|
|
1053
|
+
TorchlitOverlay as T,
|
|
1054
|
+
deepQuery as d
|
|
1055
|
+
};
|
|
1056
|
+
//# sourceMappingURL=tour-overlay-CBkFKv12.js.map
|