@trail-cli/sdk 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.js ADDED
@@ -0,0 +1,932 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/guide-loader.ts
6
+ import { parseGuideJson } from "@trail-cli/guide-schema";
7
+ async function loadGuide(url) {
8
+ let response;
9
+ try {
10
+ response = await fetch(url, { credentials: "same-origin" });
11
+ } catch {
12
+ return { success: false, kind: "network-error" };
13
+ }
14
+ if (!response.ok) {
15
+ return { success: false, kind: response.status === 404 ? "not-found" : "server-error" };
16
+ }
17
+ const text = await response.text();
18
+ const result = parseGuideJson(text);
19
+ if (!result.success) {
20
+ return { success: false, kind: "invalid", issues: result.issues };
21
+ }
22
+ return { success: true, guide: result.guide };
23
+ }
24
+
25
+ // src/session-storage.ts
26
+ var STORAGE_KEY = "trail:checkpoint";
27
+ function safeSessionStorage() {
28
+ try {
29
+ return window.sessionStorage;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function saveCheckpoint(checkpoint) {
35
+ const storage = safeSessionStorage();
36
+ if (!storage) return;
37
+ try {
38
+ storage.setItem(STORAGE_KEY, JSON.stringify(checkpoint));
39
+ } catch {
40
+ }
41
+ }
42
+ function loadCheckpoint(release, guideId) {
43
+ const storage = safeSessionStorage();
44
+ if (!storage) return null;
45
+ try {
46
+ const raw = storage.getItem(STORAGE_KEY);
47
+ if (!raw) return null;
48
+ const parsed = JSON.parse(raw);
49
+ if (parsed.release === release && parsed.guideId === guideId && typeof parsed.stepIndex === "number" && Number.isInteger(parsed.stepIndex) && typeof parsed.stepId === "string") {
50
+ return { release, guideId, stepIndex: parsed.stepIndex, stepId: parsed.stepId };
51
+ }
52
+ storage.removeItem(STORAGE_KEY);
53
+ return null;
54
+ } catch {
55
+ clearCheckpoint();
56
+ return null;
57
+ }
58
+ }
59
+ function clearCheckpoint() {
60
+ const storage = safeSessionStorage();
61
+ if (!storage) return;
62
+ try {
63
+ storage.removeItem(STORAGE_KEY);
64
+ } catch {
65
+ }
66
+ }
67
+
68
+ // src/state.ts
69
+ function resolveCheckpointIndex(guide, checkpoint) {
70
+ if (!checkpoint) return 0;
71
+ const index = guide.steps.findIndex((step) => step.id === checkpoint.stepId);
72
+ return index >= 0 ? index : null;
73
+ }
74
+ var GuideState = class {
75
+ constructor(guide, startIndex = 0) {
76
+ __publicField(this, "guide");
77
+ __publicField(this, "index");
78
+ this.guide = guide;
79
+ this.index = Math.min(Math.max(startIndex, 0), guide.steps.length - 1);
80
+ }
81
+ get currentIndex() {
82
+ return this.index;
83
+ }
84
+ get currentStep() {
85
+ return this.guide.steps[this.index];
86
+ }
87
+ get total() {
88
+ return this.guide.steps.length;
89
+ }
90
+ get isFirst() {
91
+ return this.index === 0;
92
+ }
93
+ get isLast() {
94
+ return this.index === this.guide.steps.length - 1;
95
+ }
96
+ next() {
97
+ if (this.isLast) return false;
98
+ this.index += 1;
99
+ return true;
100
+ }
101
+ back() {
102
+ if (this.isFirst) return false;
103
+ this.index -= 1;
104
+ return true;
105
+ }
106
+ reset() {
107
+ this.index = 0;
108
+ }
109
+ };
110
+
111
+ // src/highlight.ts
112
+ var PADDING = 6;
113
+ var Spotlight = class {
114
+ constructor(container) {
115
+ __publicField(this, "box");
116
+ __publicField(this, "blockers");
117
+ __publicField(this, "target", null);
118
+ __publicField(this, "resizeObserver", null);
119
+ __publicField(this, "rafHandle", null);
120
+ __publicField(this, "scheduleReposition", () => {
121
+ if (this.rafHandle !== null) return;
122
+ this.rafHandle = requestAnimationFrame(() => {
123
+ this.rafHandle = null;
124
+ this.reposition();
125
+ });
126
+ });
127
+ this.box = document.createElement("div");
128
+ this.box.className = "tg-spotlight";
129
+ this.blockers = Array.from({ length: 4 }, () => {
130
+ const blocker = document.createElement("div");
131
+ blocker.className = "tg-blocker";
132
+ container.appendChild(blocker);
133
+ return blocker;
134
+ });
135
+ container.appendChild(this.box);
136
+ window.addEventListener("resize", this.scheduleReposition);
137
+ window.addEventListener("scroll", this.scheduleReposition, true);
138
+ }
139
+ attachTo(element) {
140
+ this.detachObserver();
141
+ this.target = element;
142
+ this.resizeObserver = new ResizeObserver(this.scheduleReposition);
143
+ this.resizeObserver.observe(element);
144
+ this.box.style.display = "block";
145
+ this.blockers.forEach((blocker) => blocker.style.display = "block");
146
+ this.reposition();
147
+ }
148
+ hide() {
149
+ this.target = null;
150
+ this.detachObserver();
151
+ this.box.style.display = "none";
152
+ this.blockers.forEach((blocker) => blocker.style.display = "none");
153
+ }
154
+ destroy() {
155
+ this.hide();
156
+ window.removeEventListener("resize", this.scheduleReposition);
157
+ window.removeEventListener("scroll", this.scheduleReposition, true);
158
+ this.blockers.forEach((blocker) => blocker.remove());
159
+ this.box.remove();
160
+ }
161
+ detachObserver() {
162
+ var _a;
163
+ (_a = this.resizeObserver) == null ? void 0 : _a.disconnect();
164
+ this.resizeObserver = null;
165
+ }
166
+ reposition() {
167
+ if (!this.target) return;
168
+ const rect = this.target.getBoundingClientRect();
169
+ this.box.style.top = `${rect.top - PADDING}px`;
170
+ this.box.style.left = `${rect.left - PADDING}px`;
171
+ this.box.style.width = `${rect.width + PADDING * 2}px`;
172
+ this.box.style.height = `${rect.height + PADDING * 2}px`;
173
+ const top = Math.max(0, rect.top - PADDING);
174
+ const left = Math.max(0, rect.left - PADDING);
175
+ const right = Math.min(window.innerWidth, rect.right + PADDING);
176
+ const bottom = Math.min(window.innerHeight, rect.bottom + PADDING);
177
+ const dimensions = [
178
+ [0, 0, window.innerWidth, top],
179
+ [0, top, left, bottom - top],
180
+ [right, top, window.innerWidth - right, bottom - top],
181
+ [0, bottom, window.innerWidth, window.innerHeight - bottom]
182
+ ];
183
+ this.blockers.forEach((blocker, index) => {
184
+ const [blockerLeft, blockerTop, width, height] = dimensions[index];
185
+ Object.assign(blocker.style, {
186
+ left: `${blockerLeft}px`,
187
+ top: `${blockerTop}px`,
188
+ width: `${width}px`,
189
+ height: `${height}px`
190
+ });
191
+ });
192
+ }
193
+ };
194
+
195
+ // src/overlay.ts
196
+ var STYLE_ID = "trail-sdk-styles";
197
+ var STYLES = `
198
+ .tg-root { position: fixed; inset: 0; z-index: 2147483647; pointer-events: none; }
199
+ .tg-spotlight {
200
+ position: fixed;
201
+ display: none;
202
+ border: 2px solid #4f7cff;
203
+ border-radius: 6px;
204
+ box-shadow: 0 0 0 9999px rgba(15, 17, 26, 0.55);
205
+ pointer-events: none;
206
+ transition: top 120ms ease, left 120ms ease, width 120ms ease, height 120ms ease;
207
+ }
208
+ .tg-blocker { position: fixed; z-index: 0; pointer-events: auto; }
209
+ .tg-panel {
210
+ position: fixed;
211
+ right: 20px;
212
+ bottom: 20px;
213
+ z-index: 2;
214
+ width: 320px;
215
+ max-width: calc(100vw - 40px);
216
+ background: #12131a;
217
+ color: #f4f5f7;
218
+ border-radius: 12px;
219
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
220
+ padding: 16px 18px;
221
+ font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
222
+ pointer-events: auto;
223
+ }
224
+ .tg-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
225
+ .tg-progress-text { font-size: 12px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: #9aa1b5; }
226
+ .tg-exit-btn {
227
+ background: transparent; border: none; color: #9aa1b5; font-size: 18px; line-height: 1; cursor: pointer;
228
+ padding: 4px; border-radius: 6px;
229
+ }
230
+ .tg-exit-btn:hover, .tg-exit-btn:focus-visible { color: #f4f5f7; background: rgba(255,255,255,0.08); }
231
+ .tg-progress-bar { height: 4px; border-radius: 2px; background: rgba(255,255,255,0.12); overflow: hidden; margin-bottom: 12px; }
232
+ .tg-progress-fill { height: 100%; background: #4f7cff; transition: width 160ms ease; }
233
+ .tg-title { margin: 0 0 6px; font-size: 15px; font-weight: 700; }
234
+ .tg-instruction { margin: 0 0 16px; color: #d4d7e2; white-space: pre-line; }
235
+ .tg-panel-footer { display: flex; justify-content: space-between; gap: 8px; }
236
+ .tg-btn {
237
+ appearance: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06);
238
+ color: #f4f5f7; padding: 8px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer;
239
+ }
240
+ .tg-btn:hover { background: rgba(255,255,255,0.14); }
241
+ .tg-btn:focus-visible { outline: 2px solid #4f7cff; outline-offset: 2px; }
242
+ .tg-btn:disabled { opacity: 0.4; cursor: not-allowed; }
243
+ .tg-btn-primary { background: #4f7cff; border-color: #4f7cff; }
244
+ .tg-btn-primary:hover { background: #3f6ce0; }
245
+ .tg-error .tg-title { color: #ff6b6b; }
246
+ .tg-error-detail { font-size: 12px; color: #9aa1b5; margin: -8px 0 16px; }
247
+ `;
248
+ function injectStyles() {
249
+ if (document.getElementById(STYLE_ID)) return;
250
+ const style = document.createElement("style");
251
+ style.id = STYLE_ID;
252
+ style.textContent = STYLES;
253
+ document.head.appendChild(style);
254
+ }
255
+ var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
256
+ var Overlay = class {
257
+ constructor(callbacks) {
258
+ __publicField(this, "root");
259
+ __publicField(this, "panel");
260
+ __publicField(this, "spotlight");
261
+ __publicField(this, "callbacks");
262
+ __publicField(this, "keydownHandler", null);
263
+ __publicField(this, "previousBodyOverflow");
264
+ __publicField(this, "previouslyFocusedElement");
265
+ __publicField(this, "focusTarget", null);
266
+ injectStyles();
267
+ this.callbacks = callbacks;
268
+ this.previouslyFocusedElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
269
+ this.root = document.createElement("div");
270
+ this.root.className = "tg-root";
271
+ this.panel = document.createElement("div");
272
+ this.panel.className = "tg-panel";
273
+ this.panel.setAttribute("role", "dialog");
274
+ this.panel.setAttribute("aria-label", "Trail guide");
275
+ this.panel.setAttribute("aria-live", "polite");
276
+ this.panel.tabIndex = -1;
277
+ this.root.appendChild(this.panel);
278
+ this.spotlight = new Spotlight(this.root);
279
+ document.body.appendChild(this.root);
280
+ this.previousBodyOverflow = document.body.style.overflow;
281
+ document.body.style.overflow = "hidden";
282
+ this.keydownHandler = (event) => {
283
+ if (event.key === "Escape") {
284
+ event.preventDefault();
285
+ this.callbacks.onExit();
286
+ return;
287
+ }
288
+ if (event.key === "Tab") {
289
+ this.trapTabFocus(event);
290
+ }
291
+ };
292
+ document.addEventListener("keydown", this.keydownHandler, true);
293
+ }
294
+ /** Keeps focus cycling between the guided target and the panel controls. */
295
+ trapTabFocus(event) {
296
+ var _a;
297
+ const panelFocusable = Array.from(this.panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
298
+ (el) => el.offsetParent !== null || el === document.activeElement
299
+ );
300
+ const focusTarget = this.focusTarget && this.focusTarget.isConnected && this.focusTarget.matches(FOCUSABLE_SELECTOR) && (this.focusTarget.offsetParent !== null || this.focusTarget === document.activeElement) ? this.focusTarget : null;
301
+ const focusable = focusTarget ? [focusTarget, ...panelFocusable] : panelFocusable;
302
+ if (focusable.length === 0) {
303
+ event.preventDefault();
304
+ this.focusPanel();
305
+ return;
306
+ }
307
+ const first = focusable[0];
308
+ const last = focusable[focusable.length - 1];
309
+ const active = document.activeElement;
310
+ if (active === this.panel || !focusable.includes(active)) {
311
+ event.preventDefault();
312
+ (event.shiftKey ? last : first).focus();
313
+ return;
314
+ }
315
+ if (focusTarget && active === focusTarget) {
316
+ event.preventDefault();
317
+ (event.shiftKey ? last : (_a = panelFocusable[0]) != null ? _a : focusTarget).focus();
318
+ return;
319
+ }
320
+ if (event.shiftKey && focusTarget && active === panelFocusable[0]) {
321
+ event.preventDefault();
322
+ focusTarget.focus();
323
+ return;
324
+ }
325
+ if (event.shiftKey && active === first) {
326
+ event.preventDefault();
327
+ last.focus();
328
+ } else if (!event.shiftKey && active === last) {
329
+ event.preventDefault();
330
+ first.focus();
331
+ }
332
+ }
333
+ renderStep(model) {
334
+ const progressPct = Math.round(model.stepNumber / model.totalSteps * 100);
335
+ this.panel.className = "tg-panel";
336
+ this.panel.innerHTML = `
337
+ <div class="tg-panel-header">
338
+ <span class="tg-progress-text">Step ${model.stepNumber} of ${model.totalSteps}</span>
339
+ <button type="button" class="tg-exit-btn" aria-label="Exit Trail guide">&times;</button>
340
+ </div>
341
+ <div class="tg-progress-bar"><div class="tg-progress-fill" style="width:${progressPct}%"></div></div>
342
+ <h2 class="tg-title"></h2>
343
+ <p class="tg-instruction"></p>
344
+ <div class="tg-panel-footer">
345
+ <button type="button" class="tg-btn tg-back-btn">Back</button>
346
+ <button type="button" class="tg-btn tg-btn-primary tg-next-btn"></button>
347
+ </div>
348
+ `;
349
+ this.panel.querySelector(".tg-title").textContent = model.title;
350
+ this.panel.querySelector(".tg-instruction").textContent = model.instruction;
351
+ const backBtn = this.panel.querySelector(".tg-back-btn");
352
+ backBtn.disabled = !model.canGoBack;
353
+ backBtn.addEventListener("click", () => this.callbacks.onBack());
354
+ const nextBtn = this.panel.querySelector(".tg-next-btn");
355
+ nextBtn.disabled = !model.canGoNext;
356
+ nextBtn.textContent = model.nextLabel;
357
+ nextBtn.addEventListener("click", () => this.callbacks.onNext());
358
+ this.panel.querySelector(".tg-exit-btn").addEventListener("click", () => this.callbacks.onExit());
359
+ this.focusPanel();
360
+ }
361
+ setFocusTarget(element) {
362
+ this.focusTarget = element;
363
+ }
364
+ scrollToTarget(element) {
365
+ document.body.style.overflow = this.previousBodyOverflow;
366
+ element.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
367
+ document.body.style.overflow = "hidden";
368
+ }
369
+ setNextEnabled(enabled) {
370
+ const nextBtn = this.panel.querySelector(".tg-next-btn");
371
+ if (nextBtn) nextBtn.disabled = !enabled;
372
+ }
373
+ renderLocating(model) {
374
+ this.spotlight.hide();
375
+ this.panel.className = "tg-panel";
376
+ this.panel.innerHTML = `
377
+ <div class="tg-panel-header">
378
+ <span class="tg-progress-text">Step ${model.stepNumber} of ${model.totalSteps}</span>
379
+ <button type="button" class="tg-exit-btn" aria-label="Exit Trail guide">&times;</button>
380
+ </div>
381
+ <h2 class="tg-title"></h2>
382
+ <p class="tg-instruction"></p>
383
+ <p class="tg-error-detail">Locating element on the page&hellip;</p>
384
+ `;
385
+ this.panel.querySelector(".tg-title").textContent = model.title;
386
+ this.panel.querySelector(".tg-instruction").textContent = model.instruction;
387
+ this.panel.querySelector(".tg-exit-btn").addEventListener("click", () => this.callbacks.onExit());
388
+ this.focusPanel();
389
+ }
390
+ renderError(model) {
391
+ var _a;
392
+ this.spotlight.hide();
393
+ this.panel.className = "tg-panel tg-error";
394
+ this.panel.innerHTML = `
395
+ <div class="tg-panel-header">
396
+ <span class="tg-progress-text">Trail</span>
397
+ <button type="button" class="tg-exit-btn" aria-label="Exit Trail guide">&times;</button>
398
+ </div>
399
+ <h2 class="tg-title"></h2>
400
+ <p class="tg-instruction"></p>
401
+ ${model.detail ? `<p class="tg-error-detail"></p>` : ""}
402
+ <div class="tg-panel-footer">
403
+ ${model.showRetry ? '<button type="button" class="tg-btn tg-retry-btn">Retry</button>' : "<span></span>"}
404
+ <button type="button" class="tg-btn tg-btn-primary tg-exit-btn2">Exit</button>
405
+ </div>
406
+ `;
407
+ this.panel.querySelector(".tg-title").textContent = model.title;
408
+ this.panel.querySelector(".tg-instruction").textContent = model.message;
409
+ if (model.detail) {
410
+ this.panel.querySelector(".tg-error-detail").textContent = model.detail;
411
+ }
412
+ this.panel.querySelector(".tg-exit-btn").addEventListener("click", () => this.callbacks.onExit());
413
+ this.panel.querySelector(".tg-exit-btn2").addEventListener("click", () => this.callbacks.onExit());
414
+ (_a = this.panel.querySelector(".tg-retry-btn")) == null ? void 0 : _a.addEventListener("click", () => this.callbacks.onRetry());
415
+ this.focusPanel();
416
+ }
417
+ renderComplete(title, totalSteps, onRestart) {
418
+ this.spotlight.hide();
419
+ this.panel.className = "tg-panel";
420
+ this.panel.innerHTML = `
421
+ <h2 class="tg-title">Trail Guide Complete</h2>
422
+ <p class="tg-instruction"></p>
423
+ <div class="tg-panel-footer">
424
+ <button type="button" class="tg-btn tg-restart-btn">Restart</button>
425
+ <button type="button" class="tg-btn tg-btn-primary tg-exit-btn">Exit</button>
426
+ </div>
427
+ `;
428
+ this.panel.querySelector(".tg-instruction").textContent = `${title} \u2014 ${totalSteps} of ${totalSteps} steps completed.`;
429
+ this.panel.querySelector(".tg-restart-btn").addEventListener("click", onRestart);
430
+ this.panel.querySelector(".tg-exit-btn").addEventListener("click", () => this.callbacks.onExit());
431
+ this.focusPanel();
432
+ }
433
+ focusPanel() {
434
+ this.panel.focus({ preventScroll: true });
435
+ }
436
+ destroy() {
437
+ var _a;
438
+ this.spotlight.destroy();
439
+ if (this.keydownHandler) {
440
+ document.removeEventListener("keydown", this.keydownHandler, true);
441
+ }
442
+ document.body.style.overflow = this.previousBodyOverflow;
443
+ this.root.remove();
444
+ if ((_a = this.previouslyFocusedElement) == null ? void 0 : _a.isConnected) {
445
+ this.previouslyFocusedElement.focus({ preventScroll: true });
446
+ }
447
+ }
448
+ };
449
+
450
+ // src/target-resolver.ts
451
+ import { TARGET_STRATEGY_ORDER } from "@trail-cli/guide-schema";
452
+ var IMPLICIT_ROLE_SELECTORS = {
453
+ button: 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]',
454
+ link: 'a[href], [role="link"]',
455
+ textbox: 'input:not([type]), input[type="text"], input[type="email"], input[type="search"], input[type="tel"], input[type="url"], input[type="password"], textarea, [role="textbox"]',
456
+ checkbox: 'input[type="checkbox"], [role="checkbox"]',
457
+ radio: 'input[type="radio"], [role="radio"]',
458
+ combobox: 'select, [role="combobox"]',
459
+ option: 'option, [role="option"]',
460
+ heading: 'h1, h2, h3, h4, h5, h6, [role="heading"]',
461
+ tab: '[role="tab"]'
462
+ };
463
+ function normalize(text) {
464
+ return (text != null ? text : "").trim().replace(/\s+/g, " ").toLowerCase();
465
+ }
466
+ function isVisible(el) {
467
+ if (!(el instanceof HTMLElement)) return false;
468
+ if (el.hidden) return false;
469
+ const style = window.getComputedStyle(el);
470
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
471
+ }
472
+ function cssEscape(value) {
473
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
474
+ return value.replace(/([^\w-])/g, "\\$1");
475
+ }
476
+ function accessibleName(el) {
477
+ const ariaLabel = el.getAttribute("aria-label");
478
+ if (ariaLabel) return normalize(ariaLabel);
479
+ const labelledBy = el.getAttribute("aria-labelledby");
480
+ if (labelledBy) {
481
+ const text = labelledBy.split(/\s+/).map((id) => {
482
+ var _a, _b;
483
+ return (_b = (_a = document.getElementById(id)) == null ? void 0 : _a.textContent) != null ? _b : "";
484
+ }).join(" ");
485
+ if (text.trim()) return normalize(text);
486
+ }
487
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
488
+ if (el.value) return normalize(el.value);
489
+ if (el.placeholder) return normalize(el.placeholder);
490
+ }
491
+ return normalize(el.textContent);
492
+ }
493
+ function findByTestId(testId) {
494
+ const element = document.querySelector(
495
+ `[data-testid="${cssEscape(testId)}"]`
496
+ );
497
+ return { strategy: "testId", detail: `[data-testid="${testId}"]`, found: !!element, element };
498
+ }
499
+ function findByRole(role, name) {
500
+ var _a, _b;
501
+ const selector = (_a = IMPLICIT_ROLE_SELECTORS[role]) != null ? _a : `[role="${role}"]`;
502
+ const candidates = Array.from(document.querySelectorAll(selector)).filter(isVisible);
503
+ const target = normalize(name);
504
+ const element = (_b = candidates.find((el) => accessibleName(el) === target)) != null ? _b : null;
505
+ return { strategy: "role", detail: `role="${role}" name="${name}"`, found: !!element, element };
506
+ }
507
+ function findByLabel(label) {
508
+ const target = normalize(label);
509
+ const labels = Array.from(document.querySelectorAll("label"));
510
+ for (const labelEl of labels) {
511
+ if (normalize(labelEl.textContent) !== target) continue;
512
+ const forId = labelEl.getAttribute("for");
513
+ if (forId) {
514
+ const control = document.getElementById(forId);
515
+ if (control instanceof HTMLElement && isVisible(control)) {
516
+ return { strategy: "label", detail: `label="${label}"`, found: true, element: control };
517
+ }
518
+ }
519
+ const nested = labelEl.querySelector("input, select, textarea");
520
+ if (nested && isVisible(nested)) {
521
+ return { strategy: "label", detail: `label="${label}"`, found: true, element: nested };
522
+ }
523
+ }
524
+ return { strategy: "label", detail: `label="${label}"`, found: false, element: null };
525
+ }
526
+ var TEXT_CANDIDATE_SELECTOR = 'button, a, [role="button"], [role="link"], input[type="submit"], input[type="button"], *';
527
+ function findByText(text) {
528
+ const target = normalize(text);
529
+ let best = null;
530
+ let bestScore = Infinity;
531
+ for (const el of document.querySelectorAll(TEXT_CANDIDATE_SELECTOR)) {
532
+ if (!isVisible(el)) continue;
533
+ const ownText = normalize(el.textContent);
534
+ if (!ownText || !ownText.includes(target)) continue;
535
+ const score = el.children.length + ownText.length;
536
+ if (score < bestScore) {
537
+ best = el;
538
+ bestScore = score;
539
+ }
540
+ }
541
+ return { strategy: "text", detail: `text="${text}"`, found: !!best, element: best };
542
+ }
543
+ function findBySelector(selector) {
544
+ let element = null;
545
+ try {
546
+ element = document.querySelector(selector);
547
+ } catch {
548
+ }
549
+ return { strategy: "selector", detail: selector, found: !!element, element };
550
+ }
551
+ function resolveTarget(target) {
552
+ const attempts = [];
553
+ for (const strategy of TARGET_STRATEGY_ORDER) {
554
+ let result;
555
+ if (strategy === "testId" && target.testId) {
556
+ result = findByTestId(target.testId);
557
+ } else if (strategy === "role" && target.role && target.name) {
558
+ result = findByRole(target.role, target.name);
559
+ } else if (strategy === "label" && target.label) {
560
+ result = findByLabel(target.label);
561
+ } else if (strategy === "text" && target.text) {
562
+ result = findByText(target.text);
563
+ } else if (strategy === "selector" && target.selector) {
564
+ result = findBySelector(target.selector);
565
+ } else {
566
+ continue;
567
+ }
568
+ attempts.push({ strategy: result.strategy, detail: result.detail, found: result.found });
569
+ if (result.element) {
570
+ return { element: result.element, attempts };
571
+ }
572
+ }
573
+ return { element: null, attempts };
574
+ }
575
+
576
+ // src/dom-watcher.ts
577
+ function waitForElement(resolve, options = {}) {
578
+ var _a, _b;
579
+ const timeoutMs = (_a = options.timeoutMs) != null ? _a : 1e4;
580
+ const root = (_b = options.root) != null ? _b : document.body;
581
+ let settled = false;
582
+ let observer = null;
583
+ let timer = null;
584
+ let rafHandle = null;
585
+ const cleanup = () => {
586
+ observer == null ? void 0 : observer.disconnect();
587
+ observer = null;
588
+ if (timer !== null) clearTimeout(timer);
589
+ if (rafHandle !== null) cancelAnimationFrame(rafHandle);
590
+ };
591
+ let resolvePromise;
592
+ const promise = new Promise((res) => {
593
+ resolvePromise = res;
594
+ });
595
+ const settle = (result) => {
596
+ if (settled) return;
597
+ settled = true;
598
+ cleanup();
599
+ resolvePromise(result);
600
+ };
601
+ const attempt = () => {
602
+ if (settled) return;
603
+ const element = resolve();
604
+ if (element) {
605
+ settle({ element, timedOut: false });
606
+ }
607
+ };
608
+ const scheduleAttempt = () => {
609
+ if (rafHandle !== null) return;
610
+ rafHandle = requestAnimationFrame(() => {
611
+ rafHandle = null;
612
+ attempt();
613
+ });
614
+ };
615
+ attempt();
616
+ if (!settled) {
617
+ observer = new MutationObserver(scheduleAttempt);
618
+ observer.observe(root, { childList: true, subtree: true, attributes: true, characterData: true });
619
+ timer = setTimeout(() => settle({ element: null, timedOut: true }), timeoutMs);
620
+ }
621
+ return {
622
+ promise,
623
+ cancel: () => settle({ element: null, timedOut: false })
624
+ };
625
+ }
626
+
627
+ // src/spa-router.ts
628
+ var LOCATION_CHANGE_EVENT = "trail:locationchange";
629
+ var patched = false;
630
+ function ensurePatched() {
631
+ if (patched) return;
632
+ patched = true;
633
+ const originalPushState = history.pushState.bind(history);
634
+ const originalReplaceState = history.replaceState.bind(history);
635
+ history.pushState = function patchedPushState(...args) {
636
+ const result = originalPushState(...args);
637
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
638
+ return result;
639
+ };
640
+ history.replaceState = function patchedReplaceState(...args) {
641
+ const result = originalReplaceState(...args);
642
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
643
+ return result;
644
+ };
645
+ window.addEventListener("popstate", () => {
646
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
647
+ });
648
+ }
649
+ function onLocationChange(listener) {
650
+ ensurePatched();
651
+ window.addEventListener(LOCATION_CHANGE_EVENT, listener);
652
+ return () => window.removeEventListener(LOCATION_CHANGE_EVENT, listener);
653
+ }
654
+
655
+ // src/engine.ts
656
+ var DEFAULT_TARGET_TIMEOUT_MS = 1e4;
657
+ var TrailEngine = class {
658
+ constructor(options) {
659
+ __publicField(this, "guide");
660
+ __publicField(this, "release");
661
+ __publicField(this, "state");
662
+ __publicField(this, "overlay");
663
+ __publicField(this, "unsubscribeLocation");
664
+ __publicField(this, "onExitCallback");
665
+ __publicField(this, "cancelWait", null);
666
+ __publicField(this, "cancelStepCompletion", null);
667
+ __publicField(this, "currentStepComplete", false);
668
+ __publicField(this, "destroyed", false);
669
+ this.guide = options.guide;
670
+ this.release = options.release;
671
+ this.state = new GuideState(options.guide, options.startIndex);
672
+ this.onExitCallback = options.onExit;
673
+ this.overlay = new Overlay({
674
+ onNext: () => this.next(),
675
+ onBack: () => this.back(),
676
+ onExit: () => this.exit(),
677
+ onRetry: () => this.activateCurrentStep()
678
+ });
679
+ this.unsubscribeLocation = onLocationChange(() => {
680
+ if (!this.currentStepComplete) this.activateCurrentStep();
681
+ });
682
+ this.activateCurrentStep();
683
+ }
684
+ next() {
685
+ var _a;
686
+ if (this.destroyed || !this.currentStepComplete) return;
687
+ (_a = this.cancelWait) == null ? void 0 : _a.call(this);
688
+ if (this.state.isLast) {
689
+ this.complete();
690
+ return;
691
+ }
692
+ this.state.next();
693
+ this.activateCurrentStep();
694
+ }
695
+ back() {
696
+ var _a;
697
+ if (this.destroyed) return;
698
+ (_a = this.cancelWait) == null ? void 0 : _a.call(this);
699
+ this.state.back();
700
+ this.activateCurrentStep();
701
+ }
702
+ exit() {
703
+ var _a;
704
+ if (this.destroyed) return;
705
+ clearCheckpoint();
706
+ this.destroy();
707
+ (_a = this.onExitCallback) == null ? void 0 : _a.call(this);
708
+ }
709
+ destroy() {
710
+ var _a, _b;
711
+ if (this.destroyed) return;
712
+ this.destroyed = true;
713
+ (_a = this.cancelWait) == null ? void 0 : _a.call(this);
714
+ (_b = this.cancelStepCompletion) == null ? void 0 : _b.call(this);
715
+ this.unsubscribeLocation();
716
+ this.overlay.destroy();
717
+ }
718
+ persist() {
719
+ saveCheckpoint({
720
+ release: this.release,
721
+ guideId: this.guide.id,
722
+ stepIndex: this.state.currentIndex,
723
+ stepId: this.state.currentStep.id
724
+ });
725
+ }
726
+ activateCurrentStep() {
727
+ var _a, _b, _c;
728
+ if (this.destroyed) return;
729
+ (_a = this.cancelWait) == null ? void 0 : _a.call(this);
730
+ (_b = this.cancelStepCompletion) == null ? void 0 : _b.call(this);
731
+ this.cancelStepCompletion = null;
732
+ this.currentStepComplete = false;
733
+ this.overlay.setFocusTarget(null);
734
+ this.persist();
735
+ const step = this.state.currentStep;
736
+ if (step.action === "navigate") {
737
+ this.handleNavigateStep(step);
738
+ return;
739
+ }
740
+ const target = "target" in step ? step.target : void 0;
741
+ if (!target) {
742
+ this.renderReady(null, step);
743
+ return;
744
+ }
745
+ this.overlay.renderLocating({
746
+ stepNumber: this.state.currentIndex + 1,
747
+ totalSteps: this.state.total,
748
+ title: this.guide.title,
749
+ instruction: step.instruction
750
+ });
751
+ const timeoutMs = step.action === "wait" ? (_c = step.timeoutMs) != null ? _c : DEFAULT_TARGET_TIMEOUT_MS : DEFAULT_TARGET_TIMEOUT_MS;
752
+ const { promise, cancel } = waitForElement(() => resolveTarget(target).element, { timeoutMs });
753
+ this.cancelWait = cancel;
754
+ promise.then((result) => {
755
+ if (this.destroyed) return;
756
+ if (result.timedOut || !result.element) {
757
+ this.renderMissingTarget(step, target);
758
+ return;
759
+ }
760
+ this.overlay.scrollToTarget(result.element);
761
+ this.renderReady(result.element, step);
762
+ });
763
+ }
764
+ handleNavigateStep(step) {
765
+ const destination = new URL(step.url, window.location.origin);
766
+ if (window.location.pathname === destination.pathname) {
767
+ this.renderReady(null, step);
768
+ return;
769
+ }
770
+ saveCheckpoint({
771
+ release: this.release,
772
+ guideId: this.guide.id,
773
+ stepIndex: Math.min(this.state.currentIndex + 1, this.state.total - 1),
774
+ stepId: this.guide.steps[Math.min(this.state.currentIndex + 1, this.state.total - 1)].id
775
+ });
776
+ destination.searchParams.set("trail", this.guide.id);
777
+ destination.searchParams.set("release", this.release);
778
+ window.location.assign(destination.toString());
779
+ }
780
+ renderReady(element, step) {
781
+ this.overlay.setFocusTarget(element);
782
+ if (element) {
783
+ this.overlay.spotlight.attachTo(element);
784
+ } else {
785
+ this.overlay.spotlight.hide();
786
+ }
787
+ this.overlay.renderStep({
788
+ stepNumber: this.state.currentIndex + 1,
789
+ totalSteps: this.state.total,
790
+ title: this.guide.title,
791
+ instruction: step.instruction,
792
+ canGoBack: !this.state.isFirst,
793
+ canGoNext: this.watchStepCompletion(element, step),
794
+ nextLabel: this.state.isLast ? "Finish" : "Next"
795
+ });
796
+ }
797
+ watchStepCompletion(element, step) {
798
+ if (!element || step.action === "verify" || step.action === "wait" || step.action === "navigate") {
799
+ this.currentStepComplete = true;
800
+ return true;
801
+ }
802
+ const controller = new AbortController();
803
+ this.cancelStepCompletion = () => controller.abort();
804
+ const complete = () => {
805
+ this.currentStepComplete = true;
806
+ this.overlay.setNextEnabled(true);
807
+ if (step.autoAdvance) this.next();
808
+ };
809
+ if (step.action === "input" || step.action === "select") {
810
+ const isExpectedValue = () => (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) && element.value === step.value;
811
+ const updateCompletion = () => {
812
+ if (isExpectedValue()) {
813
+ complete();
814
+ return;
815
+ }
816
+ this.currentStepComplete = false;
817
+ this.overlay.setNextEnabled(false);
818
+ };
819
+ element.addEventListener("input", updateCompletion, { signal: controller.signal });
820
+ element.addEventListener("change", updateCompletion, { signal: controller.signal });
821
+ const initiallyComplete = isExpectedValue();
822
+ this.currentStepComplete = initiallyComplete;
823
+ if (initiallyComplete && step.autoAdvance) {
824
+ queueMicrotask(() => {
825
+ if (!controller.signal.aborted && !this.destroyed) complete();
826
+ });
827
+ }
828
+ return initiallyComplete;
829
+ }
830
+ element.addEventListener("click", complete, { once: true, signal: controller.signal });
831
+ return false;
832
+ }
833
+ renderMissingTarget(step, target) {
834
+ const { attempts } = resolveTarget(target);
835
+ const detail = attempts.map((attempt) => `${attempt.strategy}: ${attempt.detail} \u2014 ${attempt.found ? "found" : "not found"}`).join(" | ");
836
+ this.overlay.renderError({
837
+ title: "Target Not Found",
838
+ message: `${step.instruction}
839
+
840
+ The application element could not be located. Possible causes: wrong page, application still loading, UI changed, or the guide is outdated.`,
841
+ detail,
842
+ showRetry: true
843
+ });
844
+ }
845
+ complete() {
846
+ var _a;
847
+ (_a = this.cancelWait) == null ? void 0 : _a.call(this);
848
+ this.overlay.setFocusTarget(null);
849
+ clearCheckpoint();
850
+ this.overlay.renderComplete(this.guide.title, this.state.total, () => this.restart());
851
+ }
852
+ restart() {
853
+ const destination = new URL(this.guide.start.url, window.location.origin);
854
+ destination.searchParams.set("trail", this.guide.id);
855
+ destination.searchParams.set("release", this.release);
856
+ if (window.location.href === destination.toString()) {
857
+ this.state.reset();
858
+ this.activateCurrentStep();
859
+ return;
860
+ }
861
+ window.location.assign(destination.toString());
862
+ }
863
+ };
864
+
865
+ // src/index.ts
866
+ function resolveGuideRequest(options) {
867
+ var _a, _b, _c;
868
+ if (options.guideUrl) {
869
+ const params2 = new URLSearchParams(new URL(options.guideUrl, window.location.href).search);
870
+ return {
871
+ url: options.guideUrl,
872
+ guideId: (_a = params2.get("trail")) != null ? _a : "unknown",
873
+ release: (_b = params2.get("release")) != null ? _b : "unknown"
874
+ };
875
+ }
876
+ const params = new URLSearchParams(window.location.search);
877
+ const guideId = params.get("trail");
878
+ const release = params.get("release");
879
+ if (!guideId || !release) return null;
880
+ const base = ((_c = options.guideBaseUrl) != null ? _c : "/guides").replace(/\/$/, "");
881
+ return { url: `${base}/releases/${release}/${guideId}.json`, guideId, release };
882
+ }
883
+ function showStandaloneError(title, message, detail) {
884
+ const overlay = new Overlay({
885
+ onNext: () => {
886
+ },
887
+ onBack: () => {
888
+ },
889
+ onRetry: () => overlay.destroy(),
890
+ onExit: () => overlay.destroy()
891
+ });
892
+ overlay.renderError({ title, message, detail, showRetry: false });
893
+ }
894
+ async function init(options = {}) {
895
+ var _a;
896
+ const request = resolveGuideRequest(options);
897
+ if (!request) return null;
898
+ const result = await loadGuide(request.url);
899
+ if (!result.success && result.kind === "not-found") {
900
+ showStandaloneError("Trail Guide Not Found", `${request.guideId}
901
+ Release ${request.release}`);
902
+ return null;
903
+ }
904
+ if (!result.success && result.kind === "network-error") {
905
+ showStandaloneError("Unable to Load Trail Guide", "Check your network connection and try again.");
906
+ return null;
907
+ }
908
+ if (!result.success && result.kind === "server-error") {
909
+ showStandaloneError("Unable to Load Trail Guide", "The guide server returned an error. Try again later.");
910
+ return null;
911
+ }
912
+ if (!result.success) {
913
+ const detail = (_a = result.issues) == null ? void 0 : _a.map((issue) => `${issue.path}: ${issue.message}`).join(" | ");
914
+ showStandaloneError("Invalid Trail Guide", `${request.guideId} failed validation.`, detail);
915
+ return null;
916
+ }
917
+ const checkpoint = loadCheckpoint(request.release, result.guide.id);
918
+ const checkpointIndex = resolveCheckpointIndex(result.guide, checkpoint);
919
+ if (checkpoint && checkpointIndex === null) clearCheckpoint();
920
+ const startIndex = checkpointIndex != null ? checkpointIndex : 0;
921
+ return new TrailEngine({
922
+ guide: result.guide,
923
+ release: request.release,
924
+ startIndex
925
+ });
926
+ }
927
+ var Trail = { init };
928
+ export {
929
+ Trail,
930
+ resolveGuideRequest
931
+ };
932
+ //# sourceMappingURL=index.js.map