@docentjs/dom 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1274 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _docentjs_core = require("@docentjs/core");
3
+ //#region src/content.ts
4
+ const SAFE_SCHEMES = /* @__PURE__ */ new Set([
5
+ "http:",
6
+ "https:",
7
+ "mailto:",
8
+ "tel:"
9
+ ]);
10
+ function isSafeUrl(url) {
11
+ const match = /^([a-z][a-z0-9+.-]*):/i.exec(url.trim());
12
+ if (!match) return true;
13
+ return SAFE_SCHEMES.has(`${match[1]?.toLowerCase()}:`);
14
+ }
15
+ const INLINE = /(\*\*(.+?)\*\*)|(\*(.+?)\*)|(`(.+?)`)|(\[(.+?)\]\(((?:[^()\s]|\([^()]*\))+)\))/g;
16
+ function appendInline(doc, parent, text) {
17
+ let last = 0;
18
+ for (const m of text.matchAll(INLINE)) {
19
+ const index = m.index ?? 0;
20
+ if (index > last) parent.appendChild(doc.createTextNode(text.slice(last, index)));
21
+ if (m[2] !== void 0) {
22
+ const el = doc.createElement("strong");
23
+ appendInline(doc, el, m[2]);
24
+ parent.appendChild(el);
25
+ } else if (m[4] !== void 0) {
26
+ const el = doc.createElement("em");
27
+ appendInline(doc, el, m[4]);
28
+ parent.appendChild(el);
29
+ } else if (m[6] !== void 0) {
30
+ const el = doc.createElement("code");
31
+ el.textContent = m[6];
32
+ parent.appendChild(el);
33
+ } else if (m[8] !== void 0 && m[9] !== void 0) {
34
+ if (isSafeUrl(m[9])) {
35
+ const a = doc.createElement("a");
36
+ a.href = m[9];
37
+ a.target = "_blank";
38
+ a.rel = "noopener noreferrer";
39
+ appendInline(doc, a, m[8]);
40
+ parent.appendChild(a);
41
+ } else parent.appendChild(doc.createTextNode(m[8]));
42
+ }
43
+ last = index + m[0].length;
44
+ }
45
+ if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
46
+ }
47
+ function appendLines(doc, parent, text, inline) {
48
+ text.split("\n").forEach((line, i) => {
49
+ if (i > 0) parent.appendChild(doc.createElement("br"));
50
+ if (inline) appendInline(doc, parent, line);
51
+ else parent.appendChild(doc.createTextNode(line));
52
+ });
53
+ }
54
+ function renderBody(doc, body, format = "text") {
55
+ const frag = doc.createDocumentFragment();
56
+ for (const para of body.split(/\n{2,}/)) {
57
+ if (!para.trim()) continue;
58
+ const p = doc.createElement("p");
59
+ appendLines(doc, p, para, format === "markdown");
60
+ frag.appendChild(p);
61
+ }
62
+ return frag;
63
+ }
64
+ function renderMedia(doc, media) {
65
+ if (!isSafeUrl(media.src)) return null;
66
+ if (media.type === "image") {
67
+ const img = doc.createElement("img");
68
+ img.src = media.src;
69
+ img.alt = media.alt ?? "";
70
+ img.setAttribute("loading", "lazy");
71
+ return img;
72
+ }
73
+ const video = doc.createElement("video");
74
+ video.src = media.src;
75
+ video.controls = true;
76
+ video.playsInline = true;
77
+ if (media.alt) video.setAttribute("aria-label", media.alt);
78
+ return video;
79
+ }
80
+ //#endregion
81
+ //#region src/occlusion.ts
82
+ function isPinned(el) {
83
+ const view = el.ownerDocument.defaultView;
84
+ if (!view) return false;
85
+ const position = view.getComputedStyle(el).position;
86
+ return position === "fixed" || position === "sticky";
87
+ }
88
+ /** Nearest pinned ancestor (inclusive), or null. */
89
+ function pinnedAncestor(el) {
90
+ let cur = el;
91
+ while (cur && cur !== cur.ownerDocument.documentElement) {
92
+ if (isPinned(cur)) return cur;
93
+ cur = cur.parentElement;
94
+ }
95
+ return null;
96
+ }
97
+ /**
98
+ * Find a pinned element covering the target's top or bottom edge.
99
+ * `ignore` is our own host, which sits above everything.
100
+ */
101
+ function findOccluder(target, ignore, viewport) {
102
+ const doc = target.ownerDocument;
103
+ const r = target.getBoundingClientRect();
104
+ const vx = viewport.x ?? 0;
105
+ const vy = viewport.y ?? 0;
106
+ const x = Math.min(Math.max(r.left + r.width / 2, vx + 1), vx + viewport.width - 1);
107
+ const probes = [{
108
+ y: r.top + 1,
109
+ edge: "top"
110
+ }, {
111
+ y: r.bottom - 1,
112
+ edge: "bottom"
113
+ }];
114
+ for (const { y, edge } of probes) {
115
+ if (y < vy || y > vy + viewport.height) continue;
116
+ const top = doc.elementsFromPoint(x, y).filter((el) => el !== ignore)[0];
117
+ if (!top || top === target || target.contains(top) || top.contains(target)) continue;
118
+ const pinned = pinnedAncestor(top);
119
+ if (!pinned || pinned.contains(target)) continue;
120
+ return {
121
+ el: pinned,
122
+ rect: pinned.getBoundingClientRect(),
123
+ edge
124
+ };
125
+ }
126
+ return null;
127
+ }
128
+ /**
129
+ * Scroll so nothing pinned covers the target. Returns true if it scrolled.
130
+ * Runs at most twice to handle a header and a footer together.
131
+ */
132
+ function uncover(target, ignore, viewport, margin = 8) {
133
+ const win = target.ownerDocument.defaultView;
134
+ if (!win || typeof target.ownerDocument.elementsFromPoint !== "function") return false;
135
+ let scrolled = false;
136
+ for (let i = 0; i < 2; i++) {
137
+ const occluder = findOccluder(target, ignore, viewport);
138
+ if (!occluder) break;
139
+ const r = target.getBoundingClientRect();
140
+ const delta = occluder.edge === "top" ? -(occluder.rect.bottom - r.top + margin) : r.bottom - occluder.rect.top + margin;
141
+ win.scrollBy({
142
+ top: delta,
143
+ behavior: "auto"
144
+ });
145
+ scrolled = true;
146
+ }
147
+ return scrolled;
148
+ }
149
+ //#endregion
150
+ //#region src/position.ts
151
+ const OPPOSITE = {
152
+ top: "bottom",
153
+ bottom: "top",
154
+ left: "right",
155
+ right: "left"
156
+ };
157
+ function parsePlacement(placement) {
158
+ if (placement === "auto") return {
159
+ side: "auto",
160
+ align: "center"
161
+ };
162
+ const [side, align] = placement.split("-");
163
+ return {
164
+ side,
165
+ align: align ?? "center"
166
+ };
167
+ }
168
+ function clamp(value, min, max) {
169
+ return Math.min(Math.max(value, min), max);
170
+ }
171
+ function isVertical(side) {
172
+ return side === "top" || side === "bottom";
173
+ }
174
+ /** Free space between the anchor and the viewport edge on each side. */
175
+ function availableSpace(anchor, viewport) {
176
+ const vx = viewport.x ?? 0;
177
+ const vy = viewport.y ?? 0;
178
+ return {
179
+ top: anchor.y - vy,
180
+ bottom: vy + viewport.height - (anchor.y + anchor.height),
181
+ left: anchor.x - vx,
182
+ right: vx + viewport.width - (anchor.x + anchor.width)
183
+ };
184
+ }
185
+ function candidates(side, space) {
186
+ if (side === "auto") return Object.keys(space).sort((a, b) => space[b] - space[a]);
187
+ const perpendicular = isVertical(side) ? ["right", "left"] : ["bottom", "top"];
188
+ return [
189
+ side,
190
+ OPPOSITE[side],
191
+ ...perpendicular.sort((a, b) => space[b] - space[a])
192
+ ];
193
+ }
194
+ function computePosition(input) {
195
+ const { anchor, floating, viewport } = input;
196
+ const gap = input.gap ?? 12;
197
+ const edge = input.edgePadding ?? 8;
198
+ const arrowSize = input.arrowSize ?? 8;
199
+ const { side: preferred, align } = parsePlacement(input.placement);
200
+ const vx = viewport.x ?? 0;
201
+ const vy = viewport.y ?? 0;
202
+ const space = availableSpace(anchor, viewport);
203
+ const order = candidates(preferred, space);
204
+ const needed = (s) => (isVertical(s) ? floating.height : floating.width) + gap + edge;
205
+ const side = order.find((s) => space[s] >= needed(s)) ?? order[0];
206
+ let x = 0;
207
+ let y = 0;
208
+ if (side === "top") y = anchor.y - gap - floating.height;
209
+ if (side === "bottom") y = anchor.y + anchor.height + gap;
210
+ if (side === "left") x = anchor.x - gap - floating.width;
211
+ if (side === "right") x = anchor.x + anchor.width + gap;
212
+ if (isVertical(side)) {
213
+ if (align === "start") x = anchor.x;
214
+ else if (align === "end") x = anchor.x + anchor.width - floating.width;
215
+ else x = anchor.x + anchor.width / 2 - floating.width / 2;
216
+ x = clamp(x, vx + edge, Math.max(vx + edge, vx + viewport.width - edge - floating.width));
217
+ } else {
218
+ if (align === "start") y = anchor.y;
219
+ else if (align === "end") y = anchor.y + anchor.height - floating.height;
220
+ else y = anchor.y + anchor.height / 2 - floating.height / 2;
221
+ y = clamp(y, vy + edge, Math.max(vy + edge, vy + viewport.height - edge - floating.height));
222
+ }
223
+ const margin = arrowSize * 2;
224
+ const arrow = isVertical(side) ? clamp(anchor.x + anchor.width / 2 - x, margin, floating.width - margin) : clamp(anchor.y + anchor.height / 2 - y, margin, floating.height - margin);
225
+ return {
226
+ x: Math.round(x),
227
+ y: Math.round(y),
228
+ side,
229
+ align,
230
+ arrow: Math.round(arrow)
231
+ };
232
+ }
233
+ /** Centre a popover in the viewport, for steps without a target. */
234
+ function centerPosition(floating, viewport) {
235
+ return {
236
+ x: Math.round((viewport.x ?? 0) + Math.max(0, (viewport.width - floating.width) / 2)),
237
+ y: Math.round((viewport.y ?? 0) + Math.max(0, (viewport.height - floating.height) / 2))
238
+ };
239
+ }
240
+ /** Grow a rect on every side. */
241
+ function inflate(rect, by) {
242
+ return {
243
+ x: rect.x - by,
244
+ y: rect.y - by,
245
+ width: rect.width + by * 2,
246
+ height: rect.height + by * 2
247
+ };
248
+ }
249
+ /**
250
+ * The part of a rect that is on screen. Positioning against this keeps the
251
+ * popover and arrow near the visible portion of oversized targets.
252
+ */
253
+ function clipToViewport(rect, viewport) {
254
+ const vx = viewport.x ?? 0;
255
+ const vy = viewport.y ?? 0;
256
+ const x1 = Math.max(vx, rect.x);
257
+ const y1 = Math.max(vy, rect.y);
258
+ const x2 = Math.min(vx + viewport.width, rect.x + rect.width);
259
+ const y2 = Math.min(vy + viewport.height, rect.y + rect.height);
260
+ if (x2 <= x1 || y2 <= y1) return rect;
261
+ return {
262
+ x: x1,
263
+ y: y1,
264
+ width: x2 - x1,
265
+ height: y2 - y1
266
+ };
267
+ }
268
+ //#endregion
269
+ //#region src/overlay.ts
270
+ /**
271
+ * Full-viewport backdrop with a rounded cutout. The cutout is a `clip-path`
272
+ * so pointer events pass through the hole to the page for free; a separate
273
+ * blocker element covers it when the step forbids interaction.
274
+ */
275
+ function holePath(viewport, hole, radius) {
276
+ const r = Math.max(0, Math.min(radius, hole.width / 2, hole.height / 2));
277
+ const { x, y, width: w, height: h } = hole;
278
+ return `path(evenodd, "${`M0 0H${viewport.width}V${viewport.height}H0Z`}${`M${x + r} ${y}H${x + w - r}A${r} ${r} 0 0 1 ${x + w} ${y + r}V${y + h - r}A${r} ${r} 0 0 1 ${x + w - r} ${y + h}H${x + r}A${r} ${r} 0 0 1 ${x} ${y + h - r}V${y + r}A${r} ${r} 0 0 1 ${x + r} ${y}Z`}")`;
279
+ }
280
+ var Overlay = class {
281
+ el;
282
+ blocker;
283
+ lastHole = null;
284
+ constructor(doc) {
285
+ this.el = doc.createElement("div");
286
+ this.el.setAttribute("part", "overlay");
287
+ this.el.className = "overlay";
288
+ this.blocker = doc.createElement("div");
289
+ this.blocker.className = "blocker";
290
+ this.blocker.hidden = true;
291
+ }
292
+ /** Current hole, padded, in viewport coordinates. */
293
+ get hole() {
294
+ return this.lastHole;
295
+ }
296
+ update(viewport, { target, padding, radius }, block) {
297
+ if (target) this.lastHole = inflate(target, padding);
298
+ else {
299
+ const c = this.lastHole;
300
+ const cx = c ? c.x + c.width / 2 : viewport.width / 2;
301
+ const cy = c ? c.y + c.height / 2 : viewport.height / 2;
302
+ this.lastHole = null;
303
+ this.el.style.clipPath = holePath(viewport, {
304
+ x: cx,
305
+ y: cy,
306
+ width: 0,
307
+ height: 0
308
+ }, 0);
309
+ this.blocker.hidden = true;
310
+ return;
311
+ }
312
+ const hole = this.lastHole;
313
+ this.el.style.clipPath = holePath(viewport, hole, radius);
314
+ this.blocker.hidden = !block;
315
+ if (block) {
316
+ this.blocker.style.transform = `translate(${hole.x}px, ${hole.y}px)`;
317
+ this.blocker.style.width = `${hole.width}px`;
318
+ this.blocker.style.height = `${hole.height}px`;
319
+ }
320
+ }
321
+ };
322
+ //#endregion
323
+ //#region src/popover.ts
324
+ const DEFAULT_LABELS = {
325
+ next: "Next",
326
+ back: "Back",
327
+ skip: "Skip",
328
+ done: "Done",
329
+ close: "Close",
330
+ progress: "{current} of {total}"
331
+ };
332
+ function h(doc, tag, className, part) {
333
+ const el = doc.createElement(tag);
334
+ el.className = className;
335
+ el.setAttribute("part", part);
336
+ return el;
337
+ }
338
+ function slot(doc, name, fallback) {
339
+ const s = doc.createElement("slot");
340
+ s.name = name;
341
+ if (fallback) s.appendChild(fallback);
342
+ return s;
343
+ }
344
+ function formatProgress(template, current, total) {
345
+ return template.replace("{current}", String(current)).replace("{total}", String(total));
346
+ }
347
+ /**
348
+ * Resolve slot overrides into light-DOM elements carrying `slot="<name>"`.
349
+ * A `null` result projects an empty element, which suppresses the fallback.
350
+ */
351
+ function resolveSlots(doc, slots, ctx) {
352
+ const out = [];
353
+ for (const [name, render] of Object.entries(slots)) {
354
+ const content = render?.(ctx, doc);
355
+ if (content === void 0) continue;
356
+ let el;
357
+ if (content === null) el = doc.createElement("span");
358
+ else if (typeof content === "string") {
359
+ el = doc.createElement("span");
360
+ el.textContent = content;
361
+ } else if (content instanceof Element) el = content;
362
+ else {
363
+ el = doc.createElement("div");
364
+ el.appendChild(content);
365
+ }
366
+ el.setAttribute("slot", name);
367
+ out.push(el);
368
+ }
369
+ return out;
370
+ }
371
+ function buildPopover(doc, ctx, labels = {}, slots = {}) {
372
+ const { step, tour, actions } = ctx;
373
+ const options = tour.options ?? {};
374
+ const text = {
375
+ ...DEFAULT_LABELS,
376
+ ...options.labels,
377
+ ...labels
378
+ };
379
+ const buttons = step.buttons ?? {};
380
+ const id = `docent-${tour.id}-${step.id}`;
381
+ const el = h(doc, "div", "popover", "popover");
382
+ el.setAttribute("role", "dialog");
383
+ el.tabIndex = -1;
384
+ const arrow = h(doc, "div", "arrow", "arrow");
385
+ el.appendChild(arrow);
386
+ const header = h(doc, "div", "header", "header");
387
+ let titleNode;
388
+ if (step.title) {
389
+ const title = h(doc, "h2", "title", "title");
390
+ title.id = `${id}-title`;
391
+ title.textContent = step.title;
392
+ titleNode = title;
393
+ el.setAttribute("aria-labelledby", title.id);
394
+ }
395
+ header.appendChild(slot(doc, "title", titleNode));
396
+ let closeNode;
397
+ if (options.allowClose !== false && buttons.close !== false) {
398
+ const close = h(doc, "button", "close", "close");
399
+ close.type = "button";
400
+ close.setAttribute("aria-label", text.close);
401
+ close.textContent = "×";
402
+ close.addEventListener("click", () => actions.skip());
403
+ closeNode = close;
404
+ }
405
+ header.appendChild(slot(doc, "close", closeNode));
406
+ el.appendChild(slot(doc, "header", header));
407
+ let bodyNode;
408
+ if (step.body) {
409
+ const body = h(doc, "div", "body", "body");
410
+ body.id = `${id}-body`;
411
+ body.appendChild(renderBody(doc, step.body, step.format));
412
+ bodyNode = body;
413
+ el.setAttribute("aria-describedby", body.id);
414
+ }
415
+ el.appendChild(slot(doc, "body", bodyNode));
416
+ let mediaNode;
417
+ if (step.media) {
418
+ const media = renderMedia(doc, step.media);
419
+ if (media) {
420
+ const wrap = h(doc, "div", "media", "media");
421
+ wrap.appendChild(media);
422
+ mediaNode = wrap;
423
+ }
424
+ }
425
+ el.appendChild(slot(doc, "media", mediaNode));
426
+ const footer = h(doc, "div", "footer", "footer");
427
+ const progress = h(doc, "div", "progress", "progress");
428
+ if (options.showProgress !== false) progress.textContent = formatProgress(text.progress, ctx.progress.current, ctx.progress.total);
429
+ footer.appendChild(slot(doc, "progress", progress));
430
+ const group = h(doc, "div", "buttons", "buttons");
431
+ let initialFocus = el;
432
+ const button = (label, part, primary, onClick) => {
433
+ const b = h(doc, "button", primary ? "button primary" : "button", `button ${part}`);
434
+ b.type = "button";
435
+ b.textContent = label;
436
+ b.addEventListener("click", onClick);
437
+ group.appendChild(b);
438
+ return b;
439
+ };
440
+ if (buttons.back !== false && ctx.canGoBack) button(text.back, "button-back", false, actions.back);
441
+ if (buttons.skip !== false && !ctx.isLast) button(text.skip, "button-skip", false, actions.skip);
442
+ if (buttons.next !== false) initialFocus = button(ctx.isLast ? text.done : text.next, "button-next", true, actions.next);
443
+ footer.appendChild(slot(doc, "buttons", group));
444
+ el.appendChild(slot(doc, "footer", footer));
445
+ const slotted = resolveSlots(doc, slots, ctx);
446
+ if (slotted.some((s) => s.getAttribute("slot") === "buttons" || s.getAttribute("slot") === "footer")) initialFocus = el;
447
+ return {
448
+ el,
449
+ arrow,
450
+ initialFocus,
451
+ slotted
452
+ };
453
+ }
454
+ /** Wrapper used in headless mode: a positioned shell that projects the app's own popover. */
455
+ function buildHeadlessShell(doc) {
456
+ const el = h(doc, "div", "popover headless", "popover");
457
+ const arrow = h(doc, "div", "arrow", "arrow");
458
+ arrow.hidden = true;
459
+ el.appendChild(arrow);
460
+ const s = doc.createElement("slot");
461
+ s.name = "popover";
462
+ el.appendChild(s);
463
+ return {
464
+ el,
465
+ arrow
466
+ };
467
+ }
468
+ //#endregion
469
+ //#region src/styles.ts
470
+ /** Styles injected into the shadow root. Theme through the custom properties. */
471
+ const STYLES = `
472
+ :host {
473
+ --docent-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
474
+ --docent-bg: #ffffff;
475
+ --docent-fg: #111827;
476
+ --docent-muted: #6b7280;
477
+ --docent-accent: #2563eb;
478
+ --docent-accent-fg: #ffffff;
479
+ --docent-radius: 12px;
480
+ --docent-shadow: 0 10px 30px rgba(0, 0, 0, 0.18), 0 2px 6px rgba(0, 0, 0, 0.08);
481
+ --docent-width: 320px;
482
+ --docent-overlay: #000;
483
+ --docent-overlay-opacity: 0.55;
484
+ --docent-duration: 250ms;
485
+ position: fixed;
486
+ inset: 0;
487
+ z-index: var(--docent-z, 2147483000);
488
+ pointer-events: none;
489
+ font: 14px/1.5 var(--docent-font);
490
+ color: var(--docent-fg);
491
+ }
492
+ @media (prefers-color-scheme: dark) {
493
+ :host {
494
+ --docent-bg: #1f2937;
495
+ --docent-fg: #f9fafb;
496
+ --docent-muted: #9ca3af;
497
+ --docent-accent: #60a5fa;
498
+ --docent-accent-fg: #0b1220;
499
+ }
500
+ }
501
+ * { box-sizing: border-box; }
502
+ .overlay {
503
+ position: absolute;
504
+ inset: 0;
505
+ background: var(--docent-overlay);
506
+ opacity: var(--docent-overlay-opacity);
507
+ pointer-events: auto;
508
+ transition: clip-path var(--docent-duration) ease;
509
+ }
510
+ .blocker {
511
+ position: absolute;
512
+ left: 0;
513
+ top: 0;
514
+ pointer-events: auto;
515
+ }
516
+ .popover {
517
+ position: absolute;
518
+ left: 0;
519
+ top: 0;
520
+ width: var(--docent-width);
521
+ max-width: calc(100vw - 32px);
522
+ background: var(--docent-bg);
523
+ border-radius: var(--docent-radius);
524
+ box-shadow: var(--docent-shadow);
525
+ padding: 16px;
526
+ pointer-events: auto;
527
+ outline: none;
528
+ transition: transform var(--docent-duration) ease, opacity var(--docent-duration) ease;
529
+ }
530
+ .popover[data-entering] { opacity: 0; transition: none; }
531
+ .popover.headless {
532
+ width: auto;
533
+ max-width: none;
534
+ padding: 0;
535
+ background: none;
536
+ box-shadow: none;
537
+ border-radius: 0;
538
+ }
539
+ .arrow {
540
+ position: absolute;
541
+ width: 12px;
542
+ height: 12px;
543
+ background: var(--docent-bg);
544
+ transform: rotate(45deg);
545
+ }
546
+ .popover[data-side="top"] .arrow { bottom: -6px; }
547
+ .popover[data-side="bottom"] .arrow { top: -6px; }
548
+ .popover[data-side="left"] .arrow { right: -6px; }
549
+ .popover[data-side="right"] .arrow { left: -6px; }
550
+ .popover[data-side="center"] .arrow, .popover[data-side="sheet"] .arrow { display: none; }
551
+ .popover.sheet {
552
+ max-width: none;
553
+ border-radius: var(--docent-radius) var(--docent-radius) 0 0;
554
+ padding-bottom: max(16px, env(safe-area-inset-bottom));
555
+ }
556
+ .header { display: flex; align-items: flex-start; gap: 8px; }
557
+ .title { flex: 1; margin: 0; font-size: 16px; font-weight: 600; }
558
+ .close {
559
+ appearance: none;
560
+ border: 0;
561
+ background: transparent;
562
+ color: var(--docent-muted);
563
+ font-size: 18px;
564
+ line-height: 1;
565
+ cursor: pointer;
566
+ padding: 2px 4px;
567
+ margin: -4px -6px 0 0;
568
+ border-radius: 6px;
569
+ }
570
+ .close:hover, .close:focus-visible { color: var(--docent-fg); background: rgba(127, 127, 127, 0.15); }
571
+ .body { margin-top: 6px; }
572
+ .body p { margin: 0 0 8px; }
573
+ .body p:last-child { margin-bottom: 0; }
574
+ .body code {
575
+ font-family: ui-monospace, monospace;
576
+ font-size: 0.9em;
577
+ padding: 1px 4px;
578
+ border-radius: 4px;
579
+ background: rgba(127, 127, 127, 0.15);
580
+ }
581
+ .body a { color: var(--docent-accent); }
582
+ .media { margin: 10px 0 0; }
583
+ .media img, .media video { display: block; max-width: 100%; border-radius: 8px; }
584
+ .footer { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
585
+ .progress { flex: 1; color: var(--docent-muted); font-size: 12px; }
586
+ .buttons { display: flex; gap: 8px; }
587
+ .button {
588
+ appearance: none;
589
+ border: 0;
590
+ border-radius: 8px;
591
+ padding: 7px 12px;
592
+ font: inherit;
593
+ font-weight: 500;
594
+ cursor: pointer;
595
+ background: rgba(127, 127, 127, 0.15);
596
+ color: var(--docent-fg);
597
+ }
598
+ .button.primary { background: var(--docent-accent); color: var(--docent-accent-fg); }
599
+ .button:focus-visible, .close:focus-visible { outline: 2px solid var(--docent-accent); outline-offset: 2px; }
600
+ @media (prefers-reduced-motion: reduce) {
601
+ .overlay, .popover { transition: none; }
602
+ }
603
+ `;
604
+ //#endregion
605
+ //#region src/target.ts
606
+ /** Attribute that `{ name }` targets resolve through. */
607
+ const NAME_ATTRIBUTE = "data-docent";
608
+ function escapeAttr(value) {
609
+ const css = globalThis.CSS;
610
+ return css?.escape ? css.escape(value) : value.replace(/["\\]/g, "\\$&");
611
+ }
612
+ function toSpec(target) {
613
+ return typeof target === "string" ? { selectors: [target] } : target;
614
+ }
615
+ /** Selectors to try, in order, for a target. */
616
+ function candidateSelectors(target) {
617
+ const spec = toSpec(target);
618
+ const out = [];
619
+ if (spec.name) out.push(`[${NAME_ATTRIBUTE}="${escapeAttr(spec.name)}"]`);
620
+ if (spec.selectors) out.push(...spec.selectors);
621
+ return out;
622
+ }
623
+ function safeQueryAll(root, selector) {
624
+ try {
625
+ return Array.from(root.querySelectorAll(selector));
626
+ } catch {
627
+ return [];
628
+ }
629
+ }
630
+ /** Query the root, then every open shadow root beneath it. */
631
+ function queryAllDeep(root, selector) {
632
+ const direct = safeQueryAll(root, selector);
633
+ if (direct.length > 0) return direct;
634
+ const out = [];
635
+ for (const el of safeQueryAll(root, "*")) if (el.shadowRoot) out.push(...queryAllDeep(el.shadowRoot, selector));
636
+ return out;
637
+ }
638
+ function resolveTarget(target, root = document) {
639
+ const spec = toSpec(target);
640
+ let scope = root;
641
+ if (spec.within) {
642
+ const container = queryAllDeep(root, spec.within)[0];
643
+ if (!container) return null;
644
+ scope = container;
645
+ }
646
+ for (const selector of candidateSelectors(spec)) {
647
+ const matches = queryAllDeep(scope, selector);
648
+ if (matches.length > 0) return matches[spec.nth ?? 0] ?? null;
649
+ }
650
+ return null;
651
+ }
652
+ /**
653
+ * Resolve now, or watch the DOM until the target appears, the timeout passes,
654
+ * or the signal aborts. Resolves `null` when it never shows up.
655
+ */
656
+ function waitForTarget(target, timeoutMs, signal, root = document) {
657
+ const now = resolveTarget(target, root);
658
+ if (now || signal?.aborted) return Promise.resolve(now);
659
+ return new Promise((resolve) => {
660
+ let scheduled = false;
661
+ const observed = root.nodeType === Node.DOCUMENT_NODE ? root.documentElement : root;
662
+ const done = (el) => {
663
+ observer.disconnect();
664
+ if (timer !== void 0) clearTimeout(timer);
665
+ signal?.removeEventListener("abort", onAbort);
666
+ resolve(el);
667
+ };
668
+ const check = () => {
669
+ scheduled = false;
670
+ const el = resolveTarget(target, root);
671
+ if (el) done(el);
672
+ };
673
+ const observer = new MutationObserver(() => {
674
+ if (scheduled) return;
675
+ scheduled = true;
676
+ queueMicrotask(check);
677
+ });
678
+ const onAbort = () => done(null);
679
+ const timer = Number.isFinite(timeoutMs) ? setTimeout(() => done(null), timeoutMs) : void 0;
680
+ signal?.addEventListener("abort", onAbort, { once: true });
681
+ observer.observe(observed, {
682
+ childList: true,
683
+ subtree: true,
684
+ attributes: true
685
+ });
686
+ });
687
+ }
688
+ //#endregion
689
+ //#region src/theme.ts
690
+ /** Token → CSS custom property (without the `--docent-` prefix). */
691
+ const THEME_VARS = {
692
+ background: "bg",
693
+ foreground: "fg",
694
+ muted: "muted",
695
+ accent: "accent",
696
+ accentForeground: "accent-fg",
697
+ radius: "radius",
698
+ shadow: "shadow",
699
+ font: "font",
700
+ width: "width",
701
+ overlay: "overlay",
702
+ overlayOpacity: "overlay-opacity",
703
+ duration: "duration",
704
+ zIndex: "z"
705
+ };
706
+ /** Write theme tokens as inline custom properties on an element. Clears unset ones. */
707
+ function applyTheme(el, theme) {
708
+ for (const key of Object.keys(THEME_VARS)) {
709
+ const value = theme?.[key];
710
+ const prop = `--docent-${THEME_VARS[key]}`;
711
+ if (value === void 0) el.style.removeProperty(prop);
712
+ else el.style.setProperty(prop, value);
713
+ }
714
+ }
715
+ function mergeThemes(...themes) {
716
+ return Object.assign({}, ...themes.filter(Boolean));
717
+ }
718
+ //#endregion
719
+ //#region src/renderer.ts
720
+ const FOCUSABLE = "a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
721
+ var DomRenderer = class {
722
+ doc;
723
+ options;
724
+ host;
725
+ shadow;
726
+ overlay;
727
+ templateStyle;
728
+ popover;
729
+ arrow;
730
+ headlessContainer;
731
+ ctx;
732
+ target = null;
733
+ cleanups = [];
734
+ frame;
735
+ previousFocus = null;
736
+ /** Set once per step after the sheet has scrolled the target clear. */
737
+ sheetAdjusted = false;
738
+ constructor(options = {}) {
739
+ this.options = options;
740
+ this.doc = options.document ?? document;
741
+ }
742
+ hasTarget(target) {
743
+ return resolveTarget(target, this.doc) !== null;
744
+ }
745
+ async waitForTarget(target, timeoutMs, signal) {
746
+ return await waitForTarget(target, timeoutMs, signal, this.doc) !== null;
747
+ }
748
+ currentRoute() {
749
+ const { pathname, search } = this.doc.defaultView?.location ?? {
750
+ pathname: "/",
751
+ search: ""
752
+ };
753
+ return `${pathname}${search}`;
754
+ }
755
+ show(ctx) {
756
+ const firstStep = !this.host;
757
+ const host = this.mount();
758
+ this.teardownStep();
759
+ this.ctx = ctx;
760
+ this.target = ctx.step.target === void 0 ? null : resolveTarget(ctx.step.target, this.doc);
761
+ const template = this.template(ctx);
762
+ applyTheme(host, mergeThemes(this.options.theme, template?.theme, ctx.tour.options?.theme));
763
+ this.setTemplateCss(template?.css);
764
+ const initialFocus = this.options.headless ? this.buildHeadless(ctx, host, this.options.headless) : this.buildDefault(ctx, host, template);
765
+ this.popover?.setAttribute("data-entering", "");
766
+ if (this.target) {
767
+ const smooth = this.scrollIntoView(this.target, ctx.step);
768
+ if (this.options.avoidOcclusion !== false) {
769
+ const target = this.target;
770
+ this.afterScroll(smooth, () => {
771
+ if (this.target === target && uncover(target, host, this.viewport())) this.update();
772
+ });
773
+ }
774
+ }
775
+ this.update();
776
+ this.listen();
777
+ this.wireAdvance(ctx.step);
778
+ if (firstStep) this.previousFocus = this.doc.activeElement;
779
+ requestAnimationFrame(() => {
780
+ this.popover?.removeAttribute("data-entering");
781
+ initialFocus.focus({ preventScroll: true });
782
+ });
783
+ }
784
+ hide() {
785
+ this.teardownStep();
786
+ if (this.host) {
787
+ this.host.remove();
788
+ this.host = void 0;
789
+ this.shadow = void 0;
790
+ this.overlay = void 0;
791
+ this.templateStyle = void 0;
792
+ }
793
+ const prev = this.previousFocus;
794
+ this.previousFocus = null;
795
+ if (prev instanceof HTMLElement && prev.isConnected) prev.focus({ preventScroll: true });
796
+ }
797
+ /** Re-measure and re-position everything. Safe to call often. */
798
+ update() {
799
+ const ctx = this.ctx;
800
+ const overlay = this.overlay;
801
+ const popover = this.popover;
802
+ const win = this.doc.defaultView;
803
+ if (!ctx || !overlay || !popover || !win) return;
804
+ const viewport = this.viewport();
805
+ const overlaySize = {
806
+ width: overlay.el.offsetWidth,
807
+ height: overlay.el.offsetHeight
808
+ };
809
+ const spotlight = {
810
+ ...this.options.spotlight,
811
+ ...ctx.tour.options?.spotlight,
812
+ ...ctx.step.spotlight
813
+ };
814
+ const padding = spotlight.padding ?? 6;
815
+ const radius = spotlight.radius ?? 6;
816
+ const external = this.headlessContainer;
817
+ const sheet = this.isSheet(viewport);
818
+ popover.classList.toggle("sheet", sheet);
819
+ popover.style.width = sheet ? `${viewport.width}px` : "";
820
+ const floating = {
821
+ width: popover.offsetWidth,
822
+ height: popover.offsetHeight
823
+ };
824
+ if (sheet) {
825
+ const rect = this.target?.isConnected ? toRect(this.target.getBoundingClientRect()) : null;
826
+ overlay.update(overlaySize, {
827
+ target: rect,
828
+ padding,
829
+ radius
830
+ }, this.blocksInteraction(ctx.step));
831
+ const vx = viewport.x ?? 0;
832
+ const top = (viewport.y ?? 0) + viewport.height - floating.height;
833
+ popover.style.transform = `translate(${vx}px, ${top}px)`;
834
+ popover.setAttribute("data-side", "sheet");
835
+ external?.setAttribute("data-side", "sheet");
836
+ this.keepClearOfSheet(rect, top);
837
+ return;
838
+ }
839
+ if (!this.target?.isConnected) {
840
+ overlay.update(overlaySize, {
841
+ target: null,
842
+ padding,
843
+ radius
844
+ }, false);
845
+ const { x, y } = centerPosition(floating, viewport);
846
+ popover.style.transform = `translate(${x}px, ${y}px)`;
847
+ popover.setAttribute("data-side", "center");
848
+ external?.setAttribute("data-side", "center");
849
+ return;
850
+ }
851
+ const rect = toRect(this.target.getBoundingClientRect());
852
+ overlay.update(overlaySize, {
853
+ target: rect,
854
+ padding,
855
+ radius
856
+ }, this.blocksInteraction(ctx.step));
857
+ const pos = computePosition({
858
+ anchor: clipToViewport(overlay.hole ?? rect, viewport),
859
+ floating,
860
+ viewport,
861
+ placement: ctx.step.placement ?? "auto",
862
+ gap: this.options.gap ?? 12
863
+ });
864
+ popover.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
865
+ popover.setAttribute("data-side", pos.side);
866
+ if (this.arrow) {
867
+ const vertical = pos.side === "top" || pos.side === "bottom";
868
+ this.arrow.style.left = vertical ? `${pos.arrow - 6}px` : "";
869
+ this.arrow.style.top = vertical ? "" : `${pos.arrow - 6}px`;
870
+ }
871
+ if (external) {
872
+ external.setAttribute("data-side", pos.side);
873
+ external.style.setProperty("--docent-arrow", `${pos.arrow}px`);
874
+ }
875
+ }
876
+ /**
877
+ * The visible area in layout-viewport coordinates. Uses the visual viewport
878
+ * so pinch zoom, the on-screen keyboard and pages that overflow on mobile
879
+ * (where `innerWidth` grows past the screen) all position correctly.
880
+ */
881
+ viewport() {
882
+ const vv = this.doc.defaultView?.visualViewport;
883
+ if (vv) return {
884
+ x: vv.offsetLeft,
885
+ y: vv.offsetTop,
886
+ width: vv.width,
887
+ height: vv.height
888
+ };
889
+ const el = this.doc.documentElement;
890
+ return {
891
+ x: 0,
892
+ y: 0,
893
+ width: el.clientWidth,
894
+ height: el.clientHeight
895
+ };
896
+ }
897
+ isSheet(viewport) {
898
+ const breakpoint = this.options.sheetBreakpoint ?? 480;
899
+ return breakpoint > 0 && viewport.width < breakpoint;
900
+ }
901
+ /** In sheet mode, scroll once so the target is not hidden behind the sheet. */
902
+ keepClearOfSheet(target, sheetTop) {
903
+ const win = this.doc.defaultView;
904
+ if (!target || !win || this.sheetAdjusted) return;
905
+ const overlap = target.y + target.height - sheetTop;
906
+ if (overlap <= 0) return;
907
+ this.sheetAdjusted = true;
908
+ win.scrollBy({
909
+ top: overlap + 16,
910
+ behavior: "auto"
911
+ });
912
+ }
913
+ /** Run after a smooth scroll settles (scrollend, or a short fallback), or right away. */
914
+ afterScroll(smooth, fn) {
915
+ const win = this.doc.defaultView;
916
+ if (!smooth || !win) {
917
+ fn();
918
+ return;
919
+ }
920
+ let done = false;
921
+ const finish = () => {
922
+ if (done) return;
923
+ done = true;
924
+ win.removeEventListener("scrollend", finish);
925
+ clearTimeout(timer);
926
+ fn();
927
+ };
928
+ const timer = setTimeout(finish, 600);
929
+ win.addEventListener("scrollend", finish, { once: true });
930
+ this.cleanups.push(() => {
931
+ done = true;
932
+ win.removeEventListener("scrollend", finish);
933
+ clearTimeout(timer);
934
+ });
935
+ }
936
+ template(ctx) {
937
+ const name = ctx.tour.options?.template ?? this.options.template;
938
+ return name === void 0 ? void 0 : this.options.templates?.[name];
939
+ }
940
+ buildDefault(ctx, host, template) {
941
+ const slots = {
942
+ ...this.options.slots,
943
+ ...template?.slots
944
+ };
945
+ const { el, arrow, initialFocus, slotted } = buildPopover(this.doc, ctx, this.options.labels ?? {}, slots);
946
+ this.popover = el;
947
+ this.arrow = arrow;
948
+ for (const node of slotted) {
949
+ host.appendChild(node);
950
+ this.cleanups.push(() => node.remove());
951
+ }
952
+ this.shadow?.appendChild(el);
953
+ return initialFocus;
954
+ }
955
+ buildHeadless(ctx, host, headless) {
956
+ const { el, arrow } = buildHeadlessShell(this.doc);
957
+ this.popover = el;
958
+ this.arrow = arrow;
959
+ this.shadow?.appendChild(el);
960
+ const container = this.doc.createElement("div");
961
+ container.setAttribute("slot", "popover");
962
+ container.setAttribute("data-docent-popover", "");
963
+ container.setAttribute("role", "dialog");
964
+ container.tabIndex = -1;
965
+ host.appendChild(container);
966
+ this.headlessContainer = container;
967
+ const cleanup = headless.render(ctx, container);
968
+ this.cleanups.push(() => {
969
+ cleanup?.();
970
+ container.remove();
971
+ this.headlessContainer = void 0;
972
+ });
973
+ return container;
974
+ }
975
+ setTemplateCss(css) {
976
+ if (!this.shadow) return;
977
+ if (!css) {
978
+ this.templateStyle?.remove();
979
+ this.templateStyle = void 0;
980
+ return;
981
+ }
982
+ if (!this.templateStyle) {
983
+ this.templateStyle = this.doc.createElement("style");
984
+ this.shadow.appendChild(this.templateStyle);
985
+ }
986
+ if (this.templateStyle.textContent !== css) this.templateStyle.textContent = css;
987
+ }
988
+ mount() {
989
+ if (this.host) return this.host;
990
+ const host = this.doc.createElement("div");
991
+ host.setAttribute("data-docent-host", "");
992
+ const shadow = host.attachShadow({ mode: "open" });
993
+ const style = this.doc.createElement("style");
994
+ style.textContent = this.options.css ? `${STYLES}\n${this.options.css}` : STYLES;
995
+ shadow.appendChild(style);
996
+ const overlay = new Overlay(this.doc);
997
+ shadow.appendChild(overlay.el);
998
+ shadow.appendChild(overlay.blocker);
999
+ this.doc.body.appendChild(host);
1000
+ this.host = host;
1001
+ this.shadow = shadow;
1002
+ this.overlay = overlay;
1003
+ return host;
1004
+ }
1005
+ teardownStep() {
1006
+ for (const c of this.cleanups) c();
1007
+ this.cleanups = [];
1008
+ if (this.frame !== void 0) cancelAnimationFrame(this.frame);
1009
+ this.frame = void 0;
1010
+ this.popover?.remove();
1011
+ this.popover = void 0;
1012
+ this.arrow = void 0;
1013
+ this.ctx = void 0;
1014
+ this.target = null;
1015
+ this.sheetAdjusted = false;
1016
+ }
1017
+ blocksInteraction(step) {
1018
+ if (step.interaction) return step.interaction === "block";
1019
+ const advance = step.advance;
1020
+ return !(typeof advance === "object" && (advance.on === "click" || advance.on === "input"));
1021
+ }
1022
+ /** Returns true when a smooth scroll was started (callers must wait for it to settle). */
1023
+ scrollIntoView(el, step) {
1024
+ const scroll = {
1025
+ ...this.ctx?.tour.options?.scroll,
1026
+ ...step.scroll
1027
+ };
1028
+ if (scroll.enabled === false) return false;
1029
+ const r = el.getBoundingClientRect();
1030
+ const v = this.viewport();
1031
+ const vx = v.x ?? 0;
1032
+ const vy = v.y ?? 0;
1033
+ const behavior = scroll.behavior ?? "auto";
1034
+ if (r.height > v.height || r.width > v.width) {
1035
+ const topVisible = r.top >= vy && r.top < vy + v.height && r.left < vx + v.width;
1036
+ if (!topVisible) el.scrollIntoView({
1037
+ block: "start",
1038
+ inline: "start",
1039
+ behavior
1040
+ });
1041
+ return !topVisible && behavior === "smooth";
1042
+ }
1043
+ if (r.top >= vy && r.left >= vx && r.bottom <= vy + v.height && r.right <= vx + v.width) return false;
1044
+ el.scrollIntoView({
1045
+ block: scroll.block ?? "center",
1046
+ inline: "nearest",
1047
+ behavior
1048
+ });
1049
+ return behavior === "smooth";
1050
+ }
1051
+ scheduleUpdate = () => {
1052
+ if (this.frame !== void 0) return;
1053
+ this.frame = requestAnimationFrame(() => {
1054
+ this.frame = void 0;
1055
+ this.update();
1056
+ });
1057
+ };
1058
+ listen() {
1059
+ const win = this.doc.defaultView;
1060
+ const ctx = this.ctx;
1061
+ if (!win || !ctx) return;
1062
+ const on = (type, handler, opts) => {
1063
+ win.addEventListener(type, handler, opts);
1064
+ this.cleanups.push(() => win.removeEventListener(type, handler, opts));
1065
+ };
1066
+ on("scroll", this.scheduleUpdate, {
1067
+ capture: true,
1068
+ passive: true
1069
+ });
1070
+ on("resize", this.scheduleUpdate, { passive: true });
1071
+ const vv = win.visualViewport;
1072
+ if (vv) {
1073
+ vv.addEventListener("resize", this.scheduleUpdate);
1074
+ vv.addEventListener("scroll", this.scheduleUpdate);
1075
+ this.cleanups.push(() => {
1076
+ vv.removeEventListener("resize", this.scheduleUpdate);
1077
+ vv.removeEventListener("scroll", this.scheduleUpdate);
1078
+ });
1079
+ }
1080
+ if (typeof ResizeObserver !== "undefined") {
1081
+ const ro = new ResizeObserver(this.scheduleUpdate);
1082
+ if (this.target) ro.observe(this.target);
1083
+ ro.observe(this.doc.documentElement);
1084
+ if (this.popover) ro.observe(this.popover);
1085
+ if (this.headlessContainer) ro.observe(this.headlessContainer);
1086
+ this.cleanups.push(() => ro.disconnect());
1087
+ }
1088
+ const options = ctx.tour.options ?? {};
1089
+ on("keydown", (e) => this.onKeydown(e, ctx), { capture: true });
1090
+ if (options.closeOnOverlayClick && this.overlay) {
1091
+ const overlayEl = this.overlay.el;
1092
+ const handler = () => ctx.actions.skip();
1093
+ overlayEl.addEventListener("click", handler);
1094
+ this.cleanups.push(() => overlayEl.removeEventListener("click", handler));
1095
+ }
1096
+ }
1097
+ onKeydown(e, ctx) {
1098
+ const options = ctx.tour.options ?? {};
1099
+ if (e.key === "Escape" && options.allowClose !== false) {
1100
+ e.preventDefault();
1101
+ ctx.actions.skip();
1102
+ return;
1103
+ }
1104
+ if (e.key === "Tab") {
1105
+ this.trapTab(e);
1106
+ return;
1107
+ }
1108
+ if (options.keyboard === false) return;
1109
+ if (e.target instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
1110
+ if (e.key === "ArrowRight" && ctx.step.buttons?.next !== false) {
1111
+ e.preventDefault();
1112
+ ctx.actions.next();
1113
+ } else if (e.key === "ArrowLeft" && ctx.canGoBack && ctx.step.buttons?.back !== false) {
1114
+ e.preventDefault();
1115
+ ctx.actions.back();
1116
+ }
1117
+ }
1118
+ /** Keep Tab cycling inside the popover when focus is already in it. */
1119
+ trapTab(e) {
1120
+ const scope = this.headlessContainer ?? this.popover;
1121
+ if (!scope) return;
1122
+ const active = this.headlessContainer ? this.doc.activeElement : this.shadow?.activeElement;
1123
+ const inside = active && (scope.contains(active) || this.host?.contains(active));
1124
+ if (!active || !inside) return;
1125
+ const items = (this.headlessContainer ? [scope] : [scope, ...this.host ? [this.host] : []]).flatMap((r) => Array.from(r.querySelectorAll(FOCUSABLE)));
1126
+ if (items.length === 0) return;
1127
+ const first = items[0];
1128
+ const last = items[items.length - 1];
1129
+ if (e.shiftKey && active === first) {
1130
+ e.preventDefault();
1131
+ last.focus();
1132
+ } else if (!e.shiftKey && active === last) {
1133
+ e.preventDefault();
1134
+ first.focus();
1135
+ }
1136
+ }
1137
+ wireAdvance(step) {
1138
+ const advance = step.advance;
1139
+ const ctx = this.ctx;
1140
+ if (!ctx || typeof advance !== "object") return;
1141
+ if (advance.on !== "click" && advance.on !== "input") return;
1142
+ const el = advance.target === void 0 ? this.target : resolveTarget(advance.target, this.doc);
1143
+ if (!el) return;
1144
+ if (advance.on === "click") {
1145
+ const handler = () => ctx.actions.next();
1146
+ el.addEventListener("click", handler, { once: true });
1147
+ this.cleanups.push(() => el.removeEventListener("click", handler));
1148
+ return;
1149
+ }
1150
+ const pattern = advance.match ? new RegExp(advance.match) : /.+/;
1151
+ const handler = (e) => {
1152
+ const value = e.target.value ?? "";
1153
+ if (pattern.test(value)) ctx.actions.next();
1154
+ };
1155
+ el.addEventListener("input", handler);
1156
+ this.cleanups.push(() => el.removeEventListener("input", handler));
1157
+ }
1158
+ };
1159
+ function toRect(r) {
1160
+ return {
1161
+ x: r.left,
1162
+ y: r.top,
1163
+ width: r.width,
1164
+ height: r.height
1165
+ };
1166
+ }
1167
+ //#endregion
1168
+ //#region src/storage.ts
1169
+ /**
1170
+ * `localStorage`-backed adapter. Falls back to memory when storage is
1171
+ * unavailable (private mode, blocked cookies, SSR).
1172
+ */
1173
+ function createLocalStorage(storage) {
1174
+ let backing;
1175
+ try {
1176
+ backing = storage ?? globalThis.localStorage;
1177
+ const probe = "__docent__";
1178
+ backing.setItem(probe, "1");
1179
+ backing.removeItem(probe);
1180
+ } catch {
1181
+ return (0, _docentjs_core.createMemoryStorage)();
1182
+ }
1183
+ const guard = (fn, fallback) => {
1184
+ try {
1185
+ return fn();
1186
+ } catch {
1187
+ return fallback;
1188
+ }
1189
+ };
1190
+ return {
1191
+ get: (key) => guard(() => backing.getItem(key), null),
1192
+ set: (key, value) => guard(() => backing.setItem(key, value), void 0),
1193
+ remove: (key) => guard(() => backing.removeItem(key), void 0)
1194
+ };
1195
+ }
1196
+ //#endregion
1197
+ //#region src/create.ts
1198
+ /**
1199
+ * A controller pre-wired for the browser: DOM renderer, localStorage
1200
+ * persistence and route change tracking.
1201
+ */
1202
+ var DomTourController = class extends _docentjs_core.TourController {
1203
+ cleanups = [];
1204
+ constructor(tour, options = {}) {
1205
+ const { renderer: rendererOptions, followRoutes, ...rest } = options;
1206
+ const renderer = new DomRenderer(rendererOptions);
1207
+ super({
1208
+ ...rest,
1209
+ tour,
1210
+ renderer,
1211
+ storage: rest.storage ?? createLocalStorage()
1212
+ });
1213
+ if (followRoutes !== false && typeof window !== "undefined") {
1214
+ const onChange = () => void this.routeChanged();
1215
+ for (const type of ["popstate", "hashchange"]) {
1216
+ window.addEventListener(type, onChange);
1217
+ this.cleanups.push(() => window.removeEventListener(type, onChange));
1218
+ }
1219
+ const nav = window.navigation;
1220
+ if (nav) {
1221
+ nav.addEventListener("navigatesuccess", onChange);
1222
+ this.cleanups.push(() => nav.removeEventListener("navigatesuccess", onChange));
1223
+ }
1224
+ }
1225
+ }
1226
+ async destroy() {
1227
+ for (const c of this.cleanups) c();
1228
+ this.cleanups.length = 0;
1229
+ await super.destroy();
1230
+ }
1231
+ };
1232
+ /** Create a browser-ready tour. Call `.start()` or `.resume()` on the result. */
1233
+ function createTour(tour, options) {
1234
+ return new DomTourController(tour, options);
1235
+ }
1236
+ //#endregion
1237
+ exports.DEFAULT_LABELS = DEFAULT_LABELS;
1238
+ exports.DomRenderer = DomRenderer;
1239
+ exports.DomTourController = DomTourController;
1240
+ exports.NAME_ATTRIBUTE = NAME_ATTRIBUTE;
1241
+ exports.Overlay = Overlay;
1242
+ exports.THEME_VARS = THEME_VARS;
1243
+ exports.applyTheme = applyTheme;
1244
+ exports.availableSpace = availableSpace;
1245
+ exports.buildHeadlessShell = buildHeadlessShell;
1246
+ exports.buildPopover = buildPopover;
1247
+ exports.candidateSelectors = candidateSelectors;
1248
+ exports.centerPosition = centerPosition;
1249
+ exports.computePosition = computePosition;
1250
+ exports.createLocalStorage = createLocalStorage;
1251
+ exports.createTour = createTour;
1252
+ Object.defineProperty(exports, "defineTour", {
1253
+ enumerable: true,
1254
+ get: function() {
1255
+ return _docentjs_core.defineTour;
1256
+ }
1257
+ });
1258
+ exports.findOccluder = findOccluder;
1259
+ exports.formatProgress = formatProgress;
1260
+ exports.holePath = holePath;
1261
+ exports.inflate = inflate;
1262
+ exports.isSafeUrl = isSafeUrl;
1263
+ exports.mergeThemes = mergeThemes;
1264
+ exports.parsePlacement = parsePlacement;
1265
+ exports.queryAllDeep = queryAllDeep;
1266
+ exports.renderBody = renderBody;
1267
+ exports.renderMedia = renderMedia;
1268
+ exports.resolveSlots = resolveSlots;
1269
+ exports.resolveTarget = resolveTarget;
1270
+ exports.toSpec = toSpec;
1271
+ exports.uncover = uncover;
1272
+ exports.waitForTarget = waitForTarget;
1273
+
1274
+ //# sourceMappingURL=index.cjs.map