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.
Files changed (40) hide show
  1. package/README.md +19 -8
  2. package/dist/core/tour-service.d.ts +32 -0
  3. package/dist/core/tour-service.d.ts.map +1 -0
  4. package/dist/core/types.d.ts +32 -0
  5. package/dist/core/types.d.ts.map +1 -0
  6. package/dist/dom/deep-query.d.ts +2 -0
  7. package/dist/dom/deep-query.d.ts.map +1 -0
  8. package/dist/dom/positioning.d.ts +31 -0
  9. package/dist/dom/positioning.d.ts.map +1 -0
  10. package/dist/dom/scroll-manager.d.ts +4 -0
  11. package/dist/dom/scroll-manager.d.ts.map +1 -0
  12. package/dist/dom/target-resolver.d.ts +9 -0
  13. package/dist/dom/target-resolver.d.ts.map +1 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/overlay/focus-manager.d.ts +8 -0
  19. package/dist/overlay/focus-manager.d.ts.map +1 -0
  20. package/dist/overlay/step-runner.d.ts +22 -0
  21. package/dist/overlay/step-runner.d.ts.map +1 -0
  22. package/dist/overlay/types.d.ts +8 -0
  23. package/dist/overlay/types.d.ts.map +1 -0
  24. package/dist/tour-overlay-CBkFKv12.js +1056 -0
  25. package/dist/tour-overlay-CBkFKv12.js.map +1 -0
  26. package/dist/tour-overlay.d.ts +11 -27
  27. package/dist/tour-overlay.d.ts.map +1 -1
  28. package/dist/tour-overlay.js +5 -948
  29. package/dist/tour-overlay.js.map +1 -1
  30. package/dist/tour-service.d.ts +2 -60
  31. package/dist/tour-service.d.ts.map +1 -1
  32. package/dist/tour-service.js +19 -48
  33. package/dist/tour-service.js.map +1 -1
  34. package/dist/types.d.ts +1 -105
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/utils/deep-query.d.ts +1 -20
  37. package/dist/utils/deep-query.d.ts.map +1 -1
  38. package/package.json +11 -3
  39. package/dist/deep-query-vkmcq1Dw.js +0 -16
  40. 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