@theseam/ui-common 2.0.1-beta.103 → 2.0.1-beta.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1192 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, isDevMode, Injectable, inject, ElementRef, input, effect, Directive, ApplicationRef, EnvironmentInjector, createComponent, Injector, makeEnvironmentProviders, signal } from '@angular/core';
3
+ import { Subject, defer, of, throwError, ReplaySubject, EMPTY, isObservable, from } from 'rxjs';
4
+ import { filter, map, take, timeout, switchMap, catchError, tap } from 'rxjs/operators';
5
+ import { driver } from 'driver.js';
6
+
7
+ /** Injected by a component used as popover content. */
8
+ const THE_SEAM_GUIDE_CONTENT = new InjectionToken('THE_SEAM_GUIDE_CONTENT');
9
+
10
+ const THE_SEAM_GUIDE_DEFAULTS = {
11
+ dismissible: true,
12
+ targetTimeout: 3000,
13
+ onMissingTarget: 'skip',
14
+ targetLostGrace: 1000,
15
+ onTargetLost: 'elementless',
16
+ };
17
+
18
+ class TheSeamGuideBusyError extends Error {
19
+ constructor() {
20
+ super('TheSeamGuide: a non-dismissible guide is already active. Wait for' +
21
+ ' `activeGuide()?.afterClosed$` before starting another guide.');
22
+ this.name = 'TheSeamGuideBusyError';
23
+ }
24
+ }
25
+ class TheSeamGuideTargetTimeoutError extends Error {
26
+ targetName;
27
+ constructor(targetName) {
28
+ super(`TheSeamGuide: timed out waiting for target "${targetName}".`);
29
+ this.targetName = targetName;
30
+ this.name = 'TheSeamGuideTargetTimeoutError';
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Tracks elements registered by `[seamGuideTarget]` so a guide can await a
36
+ * target that does not exist yet, and notice one that disappears.
37
+ */
38
+ class TheSeamGuideTargetRegistry {
39
+ _targets = new Map();
40
+ _changes = new Subject();
41
+ /** Emits the target name whenever its registrations change. */
42
+ changes$ = this._changes.asObservable();
43
+ register(name, element) {
44
+ const list = this._targets.get(name) ?? [];
45
+ if (!list.includes(element)) {
46
+ list.push(element);
47
+ }
48
+ this._targets.set(name, list);
49
+ if (isDevMode() && list.filter((e) => e.isConnected).length > 1) {
50
+ console.warn(`TheSeamGuideTargetRegistry: more than one connected element is` +
51
+ ` registered as guide target "${name}". The most recently registered` +
52
+ ` one will be used, which may not be the one you meant.`);
53
+ }
54
+ this._changes.next(name);
55
+ }
56
+ unregister(name, element) {
57
+ const list = this._targets.get(name);
58
+ if (!list) {
59
+ return;
60
+ }
61
+ const index = list.indexOf(element);
62
+ if (index === -1) {
63
+ return;
64
+ }
65
+ list.splice(index, 1);
66
+ if (list.length === 0) {
67
+ this._targets.delete(name);
68
+ }
69
+ this._changes.next(name);
70
+ }
71
+ /** The most recently registered element for `name` that is still in the DOM. */
72
+ resolve(name) {
73
+ const list = this._targets.get(name);
74
+ if (!list) {
75
+ return null;
76
+ }
77
+ for (let i = list.length - 1; i >= 0; i--) {
78
+ if (list[i].isConnected) {
79
+ return list[i];
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ /** Emits as soon as `name` resolves. Errors with a timeout error otherwise. */
85
+ waitFor(name, timeoutMs) {
86
+ return defer(() => {
87
+ const existing = this.resolve(name);
88
+ if (existing !== null) {
89
+ return of(existing);
90
+ }
91
+ return this._changes.pipe(filter((changed) => changed === name), map(() => this.resolve(name)), filter((el) => el !== null), take(1), timeout({
92
+ first: timeoutMs,
93
+ with: () => throwError(() => new TheSeamGuideTargetTimeoutError(name)),
94
+ }));
95
+ });
96
+ }
97
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideTargetRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
98
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideTargetRegistry, providedIn: 'root' });
99
+ }
100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideTargetRegistry, decorators: [{
101
+ type: Injectable,
102
+ args: [{ providedIn: 'root' }]
103
+ }] });
104
+
105
+ /**
106
+ * Marks an element as a named guide target.
107
+ *
108
+ * Registering on init and unregistering on destroy is what lets a guide await a
109
+ * target that has not rendered yet, and recover when one is destroyed and
110
+ * recreated mid-step.
111
+ */
112
+ class TheSeamGuideTargetDirective {
113
+ _registry = inject(TheSeamGuideTargetRegistry);
114
+ _elementRef = inject(ElementRef);
115
+ seamGuideTarget = input.required(...(ngDevMode ? [{ debugName: "seamGuideTarget" }] : []));
116
+ _registeredName = null;
117
+ constructor() {
118
+ effect(() => {
119
+ const name = this.seamGuideTarget();
120
+ if (this._registeredName === name) {
121
+ return;
122
+ }
123
+ const element = this._elementRef.nativeElement;
124
+ if (this._registeredName !== null) {
125
+ this._registry.unregister(this._registeredName, element);
126
+ }
127
+ this._registry.register(name, element);
128
+ this._registeredName = name;
129
+ });
130
+ }
131
+ ngOnDestroy() {
132
+ if (this._registeredName !== null) {
133
+ this._registry.unregister(this._registeredName, this._elementRef.nativeElement);
134
+ this._registeredName = null;
135
+ }
136
+ }
137
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideTargetDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
138
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.15", type: TheSeamGuideTargetDirective, isStandalone: true, selector: "[seamGuideTarget]", inputs: { seamGuideTarget: { classPropertyName: "seamGuideTarget", publicName: "seamGuideTarget", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
139
+ }
140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideTargetDirective, decorators: [{
141
+ type: Directive,
142
+ args: [{
143
+ selector: '[seamGuideTarget]',
144
+ standalone: true,
145
+ }]
146
+ }], ctorParameters: () => [], propDecorators: { seamGuideTarget: [{ type: i0.Input, args: [{ isSignal: true, alias: "seamGuideTarget", required: true }] }] } });
147
+
148
+ const THE_SEAM_GUIDE_ADAPTER = new InjectionToken('THE_SEAM_GUIDE_ADAPTER');
149
+
150
+ /**
151
+ * Consumer-facing handle to a running guide.
152
+ *
153
+ * The caller owns this ref's lifetime. A guide is not closed automatically
154
+ * when the component that started it is destroyed — `TheSeamGuideService` is
155
+ * `providedIn: 'root'`, so its `ngOnDestroy` only fires when the root
156
+ * injector itself is destroyed, not on ordinary route/component teardown. A
157
+ * component that may be destroyed before its guide naturally ends should tie
158
+ * the ref to its own lifetime:
159
+ *
160
+ * ```ts
161
+ * const ref = this._guide.start(config)
162
+ * inject(DestroyRef).onDestroy(() => ref.close())
163
+ * ```
164
+ */
165
+ class TheSeamGuideRef {
166
+ _session;
167
+ constructor(_session) {
168
+ this._session = _session;
169
+ }
170
+ /**
171
+ * Replays every event emitted so far for this guide, from `started`
172
+ * onward — a subscriber attached at any point sees the complete history,
173
+ * not just events emitted after it subscribes. This is why subscribing
174
+ * immediately after `start()` returns still observes `started`: `start()`
175
+ * runs synchronously, but the event is not lost, it is replayed.
176
+ */
177
+ get events$() {
178
+ return this._session.events$;
179
+ }
180
+ get afterClosed$() {
181
+ return this._session.afterClosed$;
182
+ }
183
+ get activeIndex() {
184
+ return this._session.activeIndex;
185
+ }
186
+ /** Whether the user may dismiss this guide. Read by the service's concurrency rule. */
187
+ get dismissible() {
188
+ return this._session.dismissible;
189
+ }
190
+ next() {
191
+ this._session.next();
192
+ }
193
+ previous() {
194
+ this._session.previous();
195
+ }
196
+ moveTo(index) {
197
+ this._session.moveTo(index);
198
+ }
199
+ refresh() {
200
+ this._session.refresh();
201
+ }
202
+ /** Always works, including when `dismissible` is false. */
203
+ close(reason = 'dismissed') {
204
+ this._session.close(reason);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Creates and destroys the Angular view behind a popover slot.
210
+ *
211
+ * Separate from `TheSeamGuideSession` so session specs can run against a fake
212
+ * and stay free of a real `ApplicationRef`, and so the session stays free of
213
+ * rendering concerns.
214
+ */
215
+ class TheSeamGuideDomContentRenderer {
216
+ _appRef = inject(ApplicationRef);
217
+ _envInjector = inject(EnvironmentInjector);
218
+ /**
219
+ * Renders `slot` into `host`, which the caller owns.
220
+ *
221
+ * Views are attached to `ApplicationRef` rather than created through a
222
+ * `ViewContainerRef`, because this is a `providedIn: 'root'` service and
223
+ * there is no view container to reach. Attachment is what makes a view
224
+ * change-detected; where its nodes sit in the DOM is independent of it,
225
+ * which is what lets driver.js move `host` around as it rebuilds its
226
+ * popover on every render.
227
+ */
228
+ render(slot, context, host) {
229
+ if (slot.kind === 'template') {
230
+ const view = slot.template.createEmbeddedView(context);
231
+ this._appRef.attachView(view);
232
+ host.append(...view.rootNodes);
233
+ return {
234
+ destroy: () => {
235
+ this._appRef.detachView(view);
236
+ view.destroy();
237
+ },
238
+ };
239
+ }
240
+ const ref = createComponent(slot.component, {
241
+ environmentInjector: this._envInjector,
242
+ // DI rather than `setInput`: `data` is shallow-merged across three
243
+ // layers, so it routinely carries keys a given component never declared
244
+ // as an input, and `setInput` throws NG0303 for those.
245
+ elementInjector: Injector.create({
246
+ parent: this._envInjector,
247
+ providers: [
248
+ { provide: THE_SEAM_GUIDE_CONTENT, useValue: context },
249
+ { provide: TheSeamGuideRef, useValue: context.guide },
250
+ ],
251
+ }),
252
+ });
253
+ this._appRef.attachView(ref.hostView);
254
+ host.append(ref.location.nativeElement);
255
+ return {
256
+ destroy: () => {
257
+ this._appRef.detachView(ref.hostView);
258
+ ref.destroy();
259
+ },
260
+ };
261
+ }
262
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideDomContentRenderer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
263
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideDomContentRenderer, providedIn: 'root' });
264
+ }
265
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideDomContentRenderer, decorators: [{
266
+ type: Injectable,
267
+ args: [{ providedIn: 'root' }]
268
+ }] });
269
+
270
+ /**
271
+ * driver.js implementation of the guide adapter.
272
+ *
273
+ * The whole step array is handed to driver.js so its buttons, progress
274
+ * indicator, and keyboard handling are preserved, but every navigation click is
275
+ * intercepted and reported instead of acted on. The session decides what
276
+ * happens next.
277
+ */
278
+ class DriverJsGuideAdapter {
279
+ _driver = null;
280
+ start(config, callbacks) {
281
+ this.destroy();
282
+ const driverConfig = {
283
+ steps: config.steps.map((step) => this._toDriveStep(step)),
284
+ allowClose: config.allowUserDismiss,
285
+ showButtons: config.allowUserDismiss
286
+ ? ['next', 'previous', 'close']
287
+ : ['next', 'previous'],
288
+ // Intercept every navigation: driver.js must never advance itself,
289
+ // because the session owns sequencing.
290
+ onNextClick: () => callbacks.onNextRequested(),
291
+ onPrevClick: () => callbacks.onPreviousRequested(),
292
+ onCloseClick: () => callbacks.onCloseRequested(),
293
+ onDestroyStarted: () => callbacks.onCloseRequested(),
294
+ };
295
+ this._driver = driver(driverConfig);
296
+ }
297
+ next() {
298
+ this._driver?.moveNext();
299
+ }
300
+ previous() {
301
+ this._driver?.movePrevious();
302
+ }
303
+ moveTo(index) {
304
+ if (this._driver === null) {
305
+ return;
306
+ }
307
+ if (!this._driver.isActive()) {
308
+ this._driver.drive(index);
309
+ return;
310
+ }
311
+ this._driver.moveTo(index);
312
+ }
313
+ refresh() {
314
+ // driver.js's own refresh() only repositions the overlay/popover around
315
+ // the cached active element — it never re-invokes the step's element
316
+ // resolver. Re-driving the current index is what forces re-resolution,
317
+ // which is the whole point of `refresh()` for mid-step recovery.
318
+ //
319
+ // This re-drive rebuilds the popover DOM every time, producing a visible
320
+ // flash even when the resolved element hasn't changed — driver.js has no
321
+ // primitive for "re-resolve without re-render". Callers should treat this
322
+ // as a recovery operation, not something to invoke speculatively.
323
+ const index = this._driver?.getActiveIndex();
324
+ if (index !== undefined) {
325
+ this._driver?.moveTo(index);
326
+ }
327
+ }
328
+ destroy() {
329
+ if (this._driver === null) {
330
+ return;
331
+ }
332
+ const instance = this._driver;
333
+ // Null first, out of caution: if driver.js's public destroy() ever starts
334
+ // invoking onDestroyStarted synchronously, we don't want that callback
335
+ // re-entering this adapter through a non-null _driver. Today it doesn't —
336
+ // destroy() tears down with confirm=false and always skips
337
+ // onDestroyStarted — so this ordering isn't load-bearing for the current
338
+ // wiring, just defensive against a future driver.js change.
339
+ this._driver = null;
340
+ instance.destroy();
341
+ }
342
+ isActive() {
343
+ return this._driver?.isActive() ?? false;
344
+ }
345
+ _toDriveStep(step) {
346
+ // Typed honestly as `Element | undefined`, matching what the resolver
347
+ // can actually return — `undefined` is a real outcome, not an absent
348
+ // one, since the elementless path depends on it.
349
+ const resolveElement = step.element === undefined ? undefined : () => step.element?.();
350
+ return {
351
+ // driver.js's own public type only declares `() => Element`, but its
352
+ // runtime falls back to a centered popover when the resolver returns
353
+ // `undefined`. This cast crosses that documentation gap at the one
354
+ // point it matters; `resolveElement` above keeps `undefined` visible
355
+ // everywhere else in this method.
356
+ element: resolveElement,
357
+ popover: step.popover === undefined
358
+ ? undefined
359
+ : this._toDrivePopover(step.popover),
360
+ };
361
+ }
362
+ /**
363
+ * `ExhaustiveMap` makes every key of `TheSeamGuideAdapterPopover` required
364
+ * in `mapped`, so adding a field to the boundary is a compile error here
365
+ * until it is carried through.
366
+ */
367
+ _toDrivePopover(popover) {
368
+ const mapped = {
369
+ title: popover.title,
370
+ description: popover.description,
371
+ side: popover.side,
372
+ align: popover.align,
373
+ };
374
+ const { title, description, side, align } = mapped;
375
+ const hasNode = title instanceof HTMLElement || description instanceof HTMLElement;
376
+ return {
377
+ title: typeof title === 'string' ? title : undefined,
378
+ description: typeof description === 'string' ? description : undefined,
379
+ side,
380
+ align,
381
+ // driver.js hides a slot whose string is falsy, so a slot filled with a
382
+ // node must be un-hidden as well as populated. It also rebuilds the
383
+ // whole popover on every render, so this runs again on each re-drive
384
+ // and simply re-adopts the same host node.
385
+ onPopoverRender: hasNode
386
+ ? (dom) => {
387
+ if (title instanceof HTMLElement) {
388
+ dom.title.replaceChildren(title);
389
+ dom.title.style.display = 'block';
390
+ }
391
+ if (description instanceof HTMLElement) {
392
+ dom.description.replaceChildren(description);
393
+ dom.description.style.display = 'block';
394
+ }
395
+ }
396
+ : undefined,
397
+ };
398
+ }
399
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DriverJsGuideAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
400
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DriverJsGuideAdapter });
401
+ }
402
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DriverJsGuideAdapter, decorators: [{
403
+ type: Injectable
404
+ }] });
405
+
406
+ /**
407
+ * Application-wide popover defaults — the outermost of the three content
408
+ * layers. Always provided by {@link provideTheSeamGuide}, defaulting to `{}`.
409
+ */
410
+ const THE_SEAM_GUIDE_POPOVER_DEFAULTS = new InjectionToken('THE_SEAM_GUIDE_POPOVER_DEFAULTS');
411
+ /**
412
+ * Wires the guide's presentation engine.
413
+ *
414
+ * The engine is named only here — no consumer imports driver.js — so replacing
415
+ * it is a change to this call, not to application code.
416
+ */
417
+ function provideTheSeamGuide(options = {}) {
418
+ return makeEnvironmentProviders([
419
+ {
420
+ provide: THE_SEAM_GUIDE_ADAPTER,
421
+ useClass: options.adapter ?? DriverJsGuideAdapter,
422
+ },
423
+ {
424
+ provide: THE_SEAM_GUIDE_POPOVER_DEFAULTS,
425
+ useValue: options.popover ?? {},
426
+ },
427
+ ]);
428
+ }
429
+
430
+ /** A bare string is sugar for `{ text }`. */
431
+ function normalize(value) {
432
+ if (value === undefined || value === null) {
433
+ return value;
434
+ }
435
+ return typeof value === 'string' ? { text: value } : value;
436
+ }
437
+ /**
438
+ * Resolves one popover slot from its three layers. `null` means the slot is
439
+ * absent and nothing is rendered for it.
440
+ *
441
+ * Presence and content are decided separately: only the step and session
442
+ * layers can make a slot present, but once it is, all three layers contribute
443
+ * the renderer, the text, and the data. That is what lets a step say
444
+ * `title: 'Step One'` and still get the application's title component.
445
+ */
446
+ function resolveGuideContentSlot(layers) {
447
+ const presence = layers.step !== undefined ? layers.step : layers.session;
448
+ if (presence === undefined || presence === null) {
449
+ return null;
450
+ }
451
+ const provider = normalize(layers.provider);
452
+ const session = normalize(layers.session);
453
+ const step = normalize(layers.step);
454
+ const nearestFirst = [step, session, provider];
455
+ const outermostFirst = [provider, session, step];
456
+ // One search for the renderer, not one per kind: the nearest layer naming
457
+ // either wins outright, so a step's template beats a session's component.
458
+ const renderer = nearestFirst.find((layer) => layer?.template != null || layer?.component != null);
459
+ const text = nearestFirst.find((layer) => layer?.text !== undefined)?.text;
460
+ const data = {};
461
+ for (const layer of outermostFirst) {
462
+ if (layer?.data !== undefined) {
463
+ Object.assign(data, layer.data);
464
+ }
465
+ }
466
+ if (renderer?.template != null) {
467
+ return { kind: 'template', template: renderer.template, text, data };
468
+ }
469
+ if (renderer?.component != null) {
470
+ return { kind: 'component', component: renderer.component, text, data };
471
+ }
472
+ return text === undefined ? null : { kind: 'text', text };
473
+ }
474
+
475
+ class TheSeamGuideSession {
476
+ // ReplaySubject, not Subject: `start()` runs synchronously (including its
477
+ // `'started'` emit) before the service hands the ref back to the caller, so
478
+ // a subscriber that attaches right after `start()` returns is necessarily
479
+ // late. Replaying history is what lets it still observe events already
480
+ // emitted during that synchronous start, matching `_afterClosed` below.
481
+ _events = new ReplaySubject();
482
+ _afterClosed = new ReplaySubject(1);
483
+ _activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "_activeIndex" }] : []));
484
+ _transitions = new Subject();
485
+ _transitionSub = null;
486
+ _recoverySub = null;
487
+ // Bumped by every `_disarmRecovery()` call. A re-arm queued on a microtask
488
+ // captures the generation in effect when it was queued and checks it before
489
+ // acting, so a re-arm for a step the session has since moved past (a
490
+ // transition disarmed-and-rearmed for a *different* step in between) is a
491
+ // no-op instead of clobbering the newer arming.
492
+ _recoveryGeneration = 0;
493
+ _closed = false;
494
+ // Whether the currently active step's `afterStep` has already fired for
495
+ // this departure. A skip or a cancellation re-requests a transition
496
+ // without changing `_activeIndex` (the departed-from step never painted
497
+ // anything new), so without this guard the same step's `afterStep` would
498
+ // re-run on every retry. Reset to `false` whenever a step actually paints.
499
+ _afterStepFired = false;
500
+ // Index whose `beforeStep` has already been fired while its transition is
501
+ // still in flight (target not yet resolved), or `null`. driver.js leaves
502
+ // the Next/Previous buttons live during that wait, so a second click
503
+ // re-requests the same index; without this guard `switchMap` would restart
504
+ // the transition and run `beforeStep` a second time before the first
505
+ // attempt ever settles. Reset to `null` whenever a step actually paints —
506
+ // mirroring `_afterStepFired` — so a later, genuine re-entry to the same
507
+ // index still runs its `beforeStep`.
508
+ _beforeStepFiredFor = null;
509
+ steps;
510
+ options;
511
+ events$ = this._events.asObservable();
512
+ afterClosed$ = this._afterClosed.asObservable();
513
+ activeIndex = this._activeIndex.asReadonly();
514
+ _adapter;
515
+ _registry;
516
+ _contentRenderer;
517
+ _getRef;
518
+ _slots = [];
519
+ _popoverDefaults;
520
+ _sessionPopover;
521
+ _onClosed;
522
+ constructor(config, deps) {
523
+ this._adapter = deps.adapter;
524
+ this._registry = deps.registry;
525
+ this._contentRenderer = deps.contentRenderer;
526
+ this._getRef = deps.getRef;
527
+ this._popoverDefaults = deps.popoverDefaults;
528
+ this._sessionPopover = config.popover;
529
+ this._onClosed = deps.onClosed;
530
+ this.steps = config.steps;
531
+ this.options = { ...THE_SEAM_GUIDE_DEFAULTS, ...stripUndefined(config) };
532
+ this._transitionSub = this._transitions
533
+ .pipe(switchMap((request) => this._runTransition(request.index, request.direction)))
534
+ .subscribe();
535
+ }
536
+ get dismissible() {
537
+ return this.options.dismissible;
538
+ }
539
+ start() {
540
+ this._buildSlots();
541
+ this._adapter.start({
542
+ steps: this.steps.map((step, index) => this._toAdapterStep(step, index)),
543
+ allowUserDismiss: this.options.dismissible,
544
+ }, {
545
+ onNextRequested: () => this.next(),
546
+ onPreviousRequested: () => this.previous(),
547
+ onCloseRequested: () => this.close('dismissed'),
548
+ });
549
+ this._emit({ type: 'started' });
550
+ this.moveTo(0);
551
+ }
552
+ next() {
553
+ this._request(this._activeIndex() + 1, 1);
554
+ }
555
+ previous() {
556
+ this._request(this._activeIndex() - 1, -1);
557
+ }
558
+ moveTo(index) {
559
+ this._request(index, index >= this._activeIndex() ? 1 : -1);
560
+ }
561
+ /**
562
+ * Requests a transition.
563
+ *
564
+ * The emission is deferred to a microtask because `_applyMissPolicy` calls
565
+ * this from *inside* the `switchMap` projection. Emitting synchronously there
566
+ * would make the transition cancel itself mid-flight. `fakeAsync`'s `tick()`
567
+ * flushes microtasks, so specs are unaffected.
568
+ */
569
+ _request(index, direction) {
570
+ if (this._closed) {
571
+ return;
572
+ }
573
+ queueMicrotask(() => {
574
+ if (this._closed) {
575
+ return;
576
+ }
577
+ this._transitions.next({ index, direction });
578
+ });
579
+ }
580
+ refresh() {
581
+ this._adapter.refresh();
582
+ }
583
+ close(reason) {
584
+ if (this._closed) {
585
+ return;
586
+ }
587
+ this._closed = true;
588
+ this._transitionSub?.unsubscribe();
589
+ this._transitionSub = null;
590
+ this._disarmRecovery();
591
+ const result = {
592
+ reason,
593
+ lastIndex: this._activeIndex(),
594
+ };
595
+ this._adapter.destroy();
596
+ this._destroyAllSlots();
597
+ this._emit({ type: 'closed', result });
598
+ this._afterClosed.next(result);
599
+ this._afterClosed.complete();
600
+ this._events.complete();
601
+ this._onClosed(this);
602
+ }
603
+ _emit(event) {
604
+ this._events.next(event);
605
+ }
606
+ /**
607
+ * The one sequence every transition runs. Cancellable: a new request causes
608
+ * `switchMap` to unsubscribe from this, so nothing paints after teardown.
609
+ *
610
+ * A `catchError` wraps the whole sequence: a hook that throws or rejects
611
+ * must not escape to the outer `_transitions` subscriber, because that
612
+ * subscriber has no error handler of its own and an uncaught error there
613
+ * would unsubscribe it permanently, silently wedging the session (`next`,
614
+ * `previous`, and `moveTo` would become no-ops forever). A caught failure
615
+ * closes the guide rather than running the miss policy: the miss policy
616
+ * exists for a target that never appeared, not an arbitrary thrown error,
617
+ * and reusing it would (a) misreport the close reason as `'targetMissing'`
618
+ * for an `'end'`-policy step that never had a target problem, and (b) run
619
+ * `_applyMissPolicy` unprotected as the last step of an error handler,
620
+ * where a further throw (e.g. from `_paint` → `_onStepPainted`, which
621
+ * Task 7 turns into real logic) would escape to the same unhandled outer
622
+ * subscriber this whole `catchError` exists to protect.
623
+ */
624
+ _runTransition(index, direction) {
625
+ if (this._closed) {
626
+ return EMPTY;
627
+ }
628
+ this._disarmRecovery();
629
+ if (index >= this.steps.length) {
630
+ this.close('completed');
631
+ return EMPTY;
632
+ }
633
+ if (index < 0) {
634
+ return EMPTY;
635
+ }
636
+ const outgoing = this._activeIndex() >= 0 ? this.steps[this._activeIndex()] : undefined;
637
+ const incoming = this.steps[index];
638
+ const afterStep$ = this._afterStepFired
639
+ ? of(null)
640
+ : defer(() => {
641
+ this._afterStepFired = true;
642
+ return this._runHook(outgoing?.afterStep);
643
+ });
644
+ const beforeStep$ = this._beforeStepFiredFor === index
645
+ ? of(null)
646
+ : defer(() => {
647
+ this._beforeStepFiredFor = index;
648
+ return this._runHook(incoming.beforeStep);
649
+ });
650
+ return afterStep$.pipe(switchMap(() => beforeStep$), switchMap(() => this._resolveTarget(incoming)), switchMap((outcome) => {
651
+ if (this._closed) {
652
+ return EMPTY;
653
+ }
654
+ if (outcome === 'missing') {
655
+ return this._applyMissPolicy(index, incoming, direction);
656
+ }
657
+ this._paint(index, incoming);
658
+ return EMPTY;
659
+ }), catchError((err) => {
660
+ // Deliberately no call-out to `_applyMissPolicy` or anything else
661
+ // that could itself throw: this handler is the last backstop before
662
+ // the outer, handler-less `_transitions` subscription, so the whole
663
+ // body is wrapped in `try/catch` and swallows on failure — there is
664
+ // nothing left to recover to.
665
+ try {
666
+ if (this._closed) {
667
+ return EMPTY;
668
+ }
669
+ if (isDevMode()) {
670
+ console.warn(`TheSeamGuideSession: step ${index} threw during its` +
671
+ ` transition (${String(err)}); closing the guide.`);
672
+ }
673
+ this.close('destroyed');
674
+ }
675
+ catch {
676
+ // Nothing left to recover from.
677
+ }
678
+ return EMPTY;
679
+ }));
680
+ }
681
+ /** Paints a step that is actually entering: real target or elementless. */
682
+ _paint(index, step) {
683
+ const outgoing = this._activeIndex();
684
+ // Before `moveTo`: driver.js calls `onPopoverRender` synchronously from
685
+ // there, so the host must already hold its view.
686
+ this._renderSlots(index);
687
+ // A content component is given `TheSeamGuideRef` on its element injector,
688
+ // so its constructor runs synchronously inside `_renderSlots` above and
689
+ // can call `ref.close()` re-entrantly. `close()` cannot destroy the view
690
+ // `_renderSlots` just created — it is only assigned into `binding.view`
691
+ // after `render()` returns — so this must destroy it here instead, and
692
+ // must not touch the (already-destroyed) adapter or emit any further
693
+ // events for a session that is no longer open.
694
+ if (this._closed) {
695
+ this._destroySlots(index);
696
+ return;
697
+ }
698
+ this._activeIndex.set(index);
699
+ this._afterStepFired = false;
700
+ this._beforeStepFiredFor = null;
701
+ this._adapter.moveTo(index);
702
+ if (outgoing !== index) {
703
+ this._destroySlots(outgoing);
704
+ }
705
+ this._emit({ type: 'stepChanged', index, step });
706
+ this._onStepPainted(index, step);
707
+ }
708
+ _buildSlots() {
709
+ for (const step of this.steps) {
710
+ this._slots.push({
711
+ title: this._bindSlot(step, 'title'),
712
+ description: this._bindSlot(step, 'description'),
713
+ });
714
+ }
715
+ }
716
+ _bindSlot(step, name) {
717
+ const resolved = this._resolveSlot(step, name);
718
+ if (resolved === null) {
719
+ return null;
720
+ }
721
+ if (resolved.kind === 'text') {
722
+ return { kind: 'text', text: resolved.text };
723
+ }
724
+ return {
725
+ kind: 'view',
726
+ slot: resolved,
727
+ host: document.createElement('div'),
728
+ view: null,
729
+ };
730
+ }
731
+ _renderSlots(index) {
732
+ const slots = this._slots[index];
733
+ if (slots === undefined) {
734
+ return;
735
+ }
736
+ for (const binding of [slots.title, slots.description]) {
737
+ if (binding === null ||
738
+ binding.kind !== 'view' ||
739
+ binding.view !== null) {
740
+ continue;
741
+ }
742
+ binding.view = this._contentRenderer.render(binding.slot, this._contentContext(index, binding.slot), binding.host);
743
+ }
744
+ }
745
+ _destroySlots(index) {
746
+ const slots = this._slots[index];
747
+ if (slots === undefined) {
748
+ return;
749
+ }
750
+ for (const binding of [slots.title, slots.description]) {
751
+ if (binding === null ||
752
+ binding.kind !== 'view' ||
753
+ binding.view === null) {
754
+ continue;
755
+ }
756
+ binding.view.destroy();
757
+ binding.view = null;
758
+ binding.host.replaceChildren();
759
+ }
760
+ }
761
+ _destroyAllSlots() {
762
+ for (let index = 0; index < this._slots.length; index++) {
763
+ this._destroySlots(index);
764
+ }
765
+ }
766
+ /** `data` is per slot, so the context is built per slot rather than per step. */
767
+ _contentContext(index, slot) {
768
+ return {
769
+ $implicit: slot.data,
770
+ data: slot.data,
771
+ text: slot.text,
772
+ step: this.steps[index],
773
+ index,
774
+ total: this.steps.length,
775
+ guide: this._getRef(),
776
+ };
777
+ }
778
+ /**
779
+ * Arms mid-step loss detection for a painted step.
780
+ *
781
+ * Only named targets are watched — a selector or `Element` has no
782
+ * notification channel, so recovery does not apply to them in v1.
783
+ */
784
+ _onStepPainted(index, step) {
785
+ // A re-entrant `close()` from inside `_renderSlots` (see `_paint`) can
786
+ // reach here via the same synchronous call stack that `close()` itself
787
+ // triggered — `_paint` returns before calling this when it detects that
788
+ // case, but a defensive check here is what stops `TheSeamGuideTargetRegistry`
789
+ // (`providedIn: 'root'`) from ever seeing a subscription arm for a session
790
+ // that no longer exists to disarm it.
791
+ if (this._closed) {
792
+ return;
793
+ }
794
+ this._disarmRecovery();
795
+ const generation = this._recoveryGeneration;
796
+ const name = typeof step.element === 'string' ? step.element : null;
797
+ if (name === null) {
798
+ return;
799
+ }
800
+ // `_resolveNow` — not the registry's own `resolve` — is the same lookup
801
+ // that feeds the popover's resolver closure (registry, then selector
802
+ // fallback). Arming and the loss check below must agree with it: a name
803
+ // that also happens to be a live selector (e.g. `'nav'`) must not be
804
+ // treated as lost while the selector still finds it on screen.
805
+ if (this._resolveNow(step) === null) {
806
+ return;
807
+ }
808
+ this._recoverySub = this._registry.changes$
809
+ .pipe(filter((changed) => changed === name), filter(() => this._resolveNow(step) === null), take(1), switchMap(() => {
810
+ this._emit({ type: 'targetLost', index, step });
811
+ const grace = this.options.targetLostGrace;
812
+ return this._registry.waitFor(name, grace).pipe(map(() => 'recovered'), catchError(() => of('lost')));
813
+ }), tap((outcome) => {
814
+ if (this._closed) {
815
+ return;
816
+ }
817
+ if (outcome === 'recovered') {
818
+ this._adapter.refresh();
819
+ this._emit({ type: 'targetRecovered', index, step });
820
+ // Re-arm on a microtask: `_onStepPainted` calls `_disarmRecovery`,
821
+ // which would otherwise unsubscribe this subscription from inside
822
+ // its own `tap`. Guarded by generation: if the session has moved
823
+ // on (another `_disarmRecovery()` ran in between — a transition,
824
+ // or a fresher arm for this same step), this queued re-arm must
825
+ // not resurrect detection for a step that is no longer active.
826
+ queueMicrotask(() => {
827
+ if (!this._closed && this._recoveryGeneration === generation) {
828
+ this._onStepPainted(index, step);
829
+ }
830
+ });
831
+ return;
832
+ }
833
+ this._applyTargetLostPolicy(index, step);
834
+ }), catchError((err) => {
835
+ // Swallowed deliberately, not silently: a broken recovery pipeline
836
+ // must not close a guide the user is actively reading, but it
837
+ // should still be visible to whoever is developing against it.
838
+ if (isDevMode()) {
839
+ console.warn(`TheSeamGuideSession: mid-step recovery for step ${index}` +
840
+ ` threw (${String(err)}); recovery detection for this step` +
841
+ ' is now disarmed.');
842
+ }
843
+ return EMPTY;
844
+ }))
845
+ .subscribe();
846
+ }
847
+ _applyTargetLostPolicy(index, step) {
848
+ const policy = step.onTargetLost ?? this.options.onTargetLost;
849
+ if (policy === 'end') {
850
+ this.close('targetMissing');
851
+ return;
852
+ }
853
+ if (policy === 'skip') {
854
+ this._request(index + 1, 1);
855
+ return;
856
+ }
857
+ // 'elementless' — the resolver now returns undefined, so a refresh collapses
858
+ // the popover to centered without a step transition. This is terminal:
859
+ // recovery detection was already disarmed on entry to this branch (the
860
+ // `take(1)` above), and nothing re-arms it afterwards, so if the target
861
+ // returns later the popover stays centered until the next transition.
862
+ this._adapter.refresh();
863
+ }
864
+ _disarmRecovery() {
865
+ this._recoveryGeneration++;
866
+ this._recoverySub?.unsubscribe();
867
+ this._recoverySub = null;
868
+ }
869
+ _runHook(hook) {
870
+ if (hook === undefined) {
871
+ return of(null);
872
+ }
873
+ return defer(() => {
874
+ const result = hook();
875
+ if (result === undefined || result === null) {
876
+ return of(null);
877
+ }
878
+ if (isObservable(result)) {
879
+ return result.pipe(take(1));
880
+ }
881
+ return from(result);
882
+ });
883
+ }
884
+ /** Resolves `'resolved'` or `'missing'`. Never throws. */
885
+ _resolveTarget(step) {
886
+ const target = step.element;
887
+ if (target === undefined) {
888
+ return of('resolved');
889
+ }
890
+ if (typeof target !== 'string') {
891
+ const el = target instanceof Element ? target : target.nativeElement;
892
+ return of(el?.isConnected ? 'resolved' : 'missing');
893
+ }
894
+ const direct = this._registry.resolve(target);
895
+ if (direct !== null) {
896
+ return of('resolved');
897
+ }
898
+ const selectorMatch = safeQuerySelector(target);
899
+ if (selectorMatch !== null) {
900
+ return of('resolved');
901
+ }
902
+ const timeoutMs = step.targetTimeout ?? this.options.targetTimeout;
903
+ return this._registry.waitFor(target, timeoutMs).pipe(map(() => 'resolved'), catchError(() => of('missing')));
904
+ }
905
+ _applyMissPolicy(index, step, direction) {
906
+ const policy = step.onMissingTarget ?? this.options.onMissingTarget;
907
+ if (policy === 'end') {
908
+ this.close('targetMissing');
909
+ return EMPTY;
910
+ }
911
+ if (policy === 'elementless') {
912
+ this._paint(index, step);
913
+ return EMPTY;
914
+ }
915
+ if (isDevMode()) {
916
+ console.warn(`TheSeamGuideSession: skipping step ${index} because its target` +
917
+ ` "${String(step.element)}" never appeared.`);
918
+ }
919
+ this._emit({ type: 'stepSkipped', index, step });
920
+ const nextIndex = index + direction;
921
+ if (nextIndex < 0 || nextIndex >= this.steps.length) {
922
+ this.close(direction === 1 ? 'completed' : 'dismissed');
923
+ return EMPTY;
924
+ }
925
+ this._request(nextIndex, direction);
926
+ return EMPTY;
927
+ }
928
+ /** Element is a resolver function so the engine re-resolves at paint time. */
929
+ _toAdapterStep(step, index) {
930
+ return {
931
+ element: step.element === undefined
932
+ ? undefined
933
+ : () => this._resolveNow(step) ?? undefined,
934
+ popover: this._toAdapterPopover(step, index),
935
+ };
936
+ }
937
+ /**
938
+ * `ExhaustiveMap` makes every key of `TheSeamGuidePopover` required in
939
+ * `mapped`, so adding a field to the popover is a compile error here until
940
+ * it is carried through. This is the exact hop on which `side` and `align`
941
+ * were once silently dropped by a spread.
942
+ */
943
+ _toAdapterPopover(step, index) {
944
+ const slots = this._slots[index];
945
+ const mapped = {
946
+ title: slotValue(slots?.title),
947
+ description: slotValue(slots?.description),
948
+ side: this._nearestScalar(step, 'side'),
949
+ align: this._nearestScalar(step, 'align'),
950
+ };
951
+ return Object.values(mapped).every((value) => value === undefined)
952
+ ? undefined
953
+ : mapped;
954
+ }
955
+ _resolveSlot(step, name) {
956
+ return resolveGuideContentSlot({
957
+ provider: this._popoverDefaults[name],
958
+ session: this._sessionPopover?.[name],
959
+ step: step.popover?.[name],
960
+ });
961
+ }
962
+ _nearestScalar(step, key) {
963
+ return (step.popover?.[key] ??
964
+ this._sessionPopover?.[key] ??
965
+ this._popoverDefaults[key]);
966
+ }
967
+ /**
968
+ * Synchronous best-effort resolution, used by the adapter's live element
969
+ * resolver at paint time. `_resolveTarget` is the awaiting version used to
970
+ * gate transitions.
971
+ */
972
+ _resolveNow(step) {
973
+ const target = step.element;
974
+ if (target === undefined) {
975
+ return null;
976
+ }
977
+ if (typeof target === 'string') {
978
+ return this._registry.resolve(target) ?? safeQuerySelector(target);
979
+ }
980
+ if (target instanceof Element) {
981
+ return target;
982
+ }
983
+ return target.nativeElement;
984
+ }
985
+ }
986
+ function stripUndefined(config) {
987
+ const { steps: _steps, popover: _popover, ...rest } = config;
988
+ const out = {};
989
+ for (const [key, value] of Object.entries(rest)) {
990
+ if (value !== undefined) {
991
+ out[key] = value;
992
+ }
993
+ }
994
+ return out;
995
+ }
996
+ /** A text slot goes to the engine as a string; a view slot as its host node. */
997
+ function slotValue(binding) {
998
+ if (binding === null || binding === undefined) {
999
+ return undefined;
1000
+ }
1001
+ return binding.kind === 'text' ? binding.text : binding.host;
1002
+ }
1003
+ /** `querySelector` throws on an invalid selector; a registry name often is one. */
1004
+ function safeQuerySelector(selector) {
1005
+ try {
1006
+ return document.querySelector(selector);
1007
+ }
1008
+ catch {
1009
+ return null;
1010
+ }
1011
+ }
1012
+
1013
+ class TheSeamGuideService {
1014
+ _adapter = inject(THE_SEAM_GUIDE_ADAPTER);
1015
+ _registry = inject(TheSeamGuideTargetRegistry);
1016
+ _popoverDefaults = inject(THE_SEAM_GUIDE_POPOVER_DEFAULTS, { optional: true }) ?? {};
1017
+ _contentRenderer = inject(TheSeamGuideDomContentRenderer);
1018
+ _activeGuide = signal(null, ...(ngDevMode ? [{ debugName: "_activeGuide" }] : []));
1019
+ /**
1020
+ * The running guide, or null. Exposed so a caller can queue itself:
1021
+ * `activeGuide()?.afterClosed$.subscribe(() => start(next))`.
1022
+ */
1023
+ activeGuide = this._activeGuide.asReadonly();
1024
+ /**
1025
+ * Starts a guide. One runs at a time: a dismissible active guide is
1026
+ * superseded, a non-dismissible one throws `TheSeamGuideBusyError`.
1027
+ *
1028
+ * The caller owns the returned ref's lifetime. A guide is **not** closed
1029
+ * automatically when the component that started it is destroyed — only
1030
+ * when the root injector is (this service is `providedIn: 'root'`), which
1031
+ * does not happen on ordinary route/component teardown. A component that
1032
+ * starts a guide and may be destroyed before it naturally ends should tie
1033
+ * the ref to its own lifetime:
1034
+ *
1035
+ * ```ts
1036
+ * const ref = this._guide.start(config)
1037
+ * inject(DestroyRef).onDestroy(() => ref.close())
1038
+ * ```
1039
+ */
1040
+ start(config) {
1041
+ const active = this._activeGuide();
1042
+ if (active !== null) {
1043
+ if (!active.dismissible) {
1044
+ throw new TheSeamGuideBusyError();
1045
+ }
1046
+ active.close('superseded');
1047
+ }
1048
+ if (isDevMode() &&
1049
+ config.dismissible === false &&
1050
+ (config.onMissingTarget ?? THE_SEAM_GUIDE_DEFAULTS.onMissingTarget) ===
1051
+ 'skip') {
1052
+ console.warn('TheSeamGuideService: this guide sets `dismissible: false` with' +
1053
+ " `onMissingTarget: 'skip'`, so the user is forced through a guide" +
1054
+ ' that may silently drop its own steps. Consider onMissingTarget:' +
1055
+ " 'end' or 'elementless'.");
1056
+ }
1057
+ // eslint-disable-next-line prefer-const -- captured by the closure below before assignment
1058
+ let ref;
1059
+ const session = new TheSeamGuideSession(config, {
1060
+ adapter: this._adapter,
1061
+ registry: this._registry,
1062
+ contentRenderer: this._contentRenderer,
1063
+ popoverDefaults: this._popoverDefaults,
1064
+ getRef: () => ref,
1065
+ onClosed: () => this._clearIfCurrent(ref),
1066
+ });
1067
+ ref = new TheSeamGuideRef(session);
1068
+ this._activeGuide.set(ref);
1069
+ session.start();
1070
+ return ref;
1071
+ }
1072
+ /**
1073
+ * Highlights a single element. A one-step guide.
1074
+ *
1075
+ * As with {@link start}, the caller owns the returned ref's lifetime: it is
1076
+ * not closed automatically when the component that requested it is
1077
+ * destroyed. See {@link start}'s doc comment for the `DestroyRef` pattern.
1078
+ */
1079
+ highlight(step) {
1080
+ return this.start({ steps: [step] });
1081
+ }
1082
+ /**
1083
+ * Closes any active guide when the owning injector is destroyed —
1084
+ * otherwise driver.js's overlay is left in the DOM, and its
1085
+ * `pointer-events: none` blocks every click on the page with no recovery
1086
+ * short of a reload. `close` always works programmatically even when the
1087
+ * guide is `dismissible: false`, which is exactly the case that must not
1088
+ * be left behind.
1089
+ */
1090
+ ngOnDestroy() {
1091
+ this._activeGuide()?.close('destroyed');
1092
+ }
1093
+ _clearIfCurrent(ref) {
1094
+ if (this._activeGuide() === ref) {
1095
+ this._activeGuide.set(null);
1096
+ }
1097
+ }
1098
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1099
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideService, providedIn: 'root' });
1100
+ }
1101
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGuideService, decorators: [{
1102
+ type: Injectable,
1103
+ args: [{ providedIn: 'root' }]
1104
+ }] });
1105
+
1106
+ /**
1107
+ * Angular-free renderer for specs. Records what the session asked to render so
1108
+ * a test can assert view lifetime without a `TestBed`.
1109
+ */
1110
+ class TheSeamFakeGuideContentRenderer {
1111
+ renders = [];
1112
+ render(slot, context, host) {
1113
+ const record = {
1114
+ slot,
1115
+ context,
1116
+ host,
1117
+ destroyed: false,
1118
+ };
1119
+ this.renders.push(record);
1120
+ host.textContent = context.text ?? '';
1121
+ return {
1122
+ destroy: () => {
1123
+ record.destroyed = true;
1124
+ },
1125
+ };
1126
+ }
1127
+ /** Renders that have not been destroyed. */
1128
+ get live() {
1129
+ return this.renders.filter((r) => !r.destroyed);
1130
+ }
1131
+ }
1132
+
1133
+ /**
1134
+ * Engine-free adapter for specs. Records what the service asked for and lets a
1135
+ * test simulate user intent without a DOM.
1136
+ */
1137
+ class TheSeamFakeGuideAdapter {
1138
+ calls = [];
1139
+ startedConfig = null;
1140
+ _callbacks = null;
1141
+ _active = false;
1142
+ start(config, callbacks) {
1143
+ this.startedConfig = config;
1144
+ this._callbacks = callbacks;
1145
+ this._active = true;
1146
+ this.calls.push('start');
1147
+ }
1148
+ next() {
1149
+ this.calls.push('next');
1150
+ }
1151
+ previous() {
1152
+ this.calls.push('previous');
1153
+ }
1154
+ moveTo(index) {
1155
+ this.calls.push(`moveTo:${index}`);
1156
+ }
1157
+ refresh() {
1158
+ this.calls.push('refresh');
1159
+ }
1160
+ destroy() {
1161
+ this._active = false;
1162
+ // Dropped, not merely inert-by-`_active`: `emitNext`/`emitPrevious`/
1163
+ // `emitClose` call through `_callbacks` directly and do not consult
1164
+ // `_active`, so a stale reference here would still fire callbacks a test
1165
+ // simulates after destroy.
1166
+ this._callbacks = null;
1167
+ this.calls.push('destroy');
1168
+ }
1169
+ isActive() {
1170
+ return this._active;
1171
+ }
1172
+ /** Resolves the element for a step, as the engine would at paint time. */
1173
+ resolveStepElement(index) {
1174
+ return this.startedConfig?.steps[index]?.element?.();
1175
+ }
1176
+ emitNext() {
1177
+ this._callbacks?.onNextRequested();
1178
+ }
1179
+ emitPrevious() {
1180
+ this._callbacks?.onPreviousRequested();
1181
+ }
1182
+ emitClose() {
1183
+ this._callbacks?.onCloseRequested();
1184
+ }
1185
+ }
1186
+
1187
+ /**
1188
+ * Generated bundle index. Do not edit.
1189
+ */
1190
+
1191
+ export { THE_SEAM_GUIDE_ADAPTER, THE_SEAM_GUIDE_CONTENT, THE_SEAM_GUIDE_DEFAULTS, THE_SEAM_GUIDE_POPOVER_DEFAULTS, TheSeamFakeGuideAdapter, TheSeamFakeGuideContentRenderer, TheSeamGuideBusyError, TheSeamGuideRef, TheSeamGuideService, TheSeamGuideTargetDirective, TheSeamGuideTargetRegistry, TheSeamGuideTargetTimeoutError, provideTheSeamGuide };
1192
+ //# sourceMappingURL=theseam-ui-common-guide.mjs.map