@cognior/iap-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.
Potentially problematic release.
This version of @cognior/iap-sdk might be problematic. Click here for more details.
- package/.github/copilot-instructions.md +95 -0
- package/README.md +79 -0
- package/TRACKING.md +105 -0
- package/USER_CONTEXT_README.md +284 -0
- package/package.json +154 -0
- package/src/config.ts +25 -0
- package/src/core/flowEngine.ts +1833 -0
- package/src/core/triggerManager.ts +1011 -0
- package/src/experiences/banner.ts +366 -0
- package/src/experiences/beacon.ts +668 -0
- package/src/experiences/hotspotTour.ts +654 -0
- package/src/experiences/hotspots.ts +566 -0
- package/src/experiences/modal.ts +1337 -0
- package/src/experiences/modalSequence.ts +1247 -0
- package/src/experiences/popover.ts +652 -0
- package/src/experiences/registry.ts +21 -0
- package/src/experiences/survey.ts +1639 -0
- package/src/experiences/taskList.ts +625 -0
- package/src/experiences/tooltip.ts +740 -0
- package/src/experiences/types.ts +395 -0
- package/src/experiences/walkthrough.ts +670 -0
- package/src/flow-sequence.ts +177 -0
- package/src/flows.ts +512 -0
- package/src/http.ts +61 -0
- package/src/index.ts +355 -0
- package/src/services/flowManager.ts +905 -0
- package/src/services/flowNormalizer.ts +74 -0
- package/src/services/locationContextService.ts +189 -0
- package/src/services/pageContextService.ts +221 -0
- package/src/services/userContextService.ts +286 -0
- package/src/state/appState.ts +0 -0
- package/src/state/hooks.ts +0 -0
- package/src/state/index.ts +0 -0
- package/src/state/migration.ts +0 -0
- package/src/state/store.ts +0 -0
- package/src/styles/banner.css.ts +0 -0
- package/src/styles/hotspot.css.ts +0 -0
- package/src/styles/hotspotTour.css.ts +0 -0
- package/src/styles/modal.css.ts +564 -0
- package/src/styles/survey.css.ts +1013 -0
- package/src/styles/taskList.css.ts +0 -0
- package/src/styles/tooltip.css.ts +149 -0
- package/src/styles/walkthrough.css.ts +0 -0
- package/src/tourUtils.ts +0 -0
- package/src/tracking.ts +223 -0
- package/src/utils/debounce.ts +66 -0
- package/src/utils/eventSequenceValidator.ts +124 -0
- package/src/utils/flowTrackingSystem.ts +524 -0
- package/src/utils/idGenerator.ts +155 -0
- package/src/utils/immediateValidationPrevention.ts +184 -0
- package/src/utils/normalize.ts +50 -0
- package/src/utils/privacyManager.ts +166 -0
- package/src/utils/ruleEvaluator.ts +199 -0
- package/src/utils/sanitize.ts +79 -0
- package/src/utils/selectors.ts +107 -0
- package/src/utils/stepExecutor.ts +345 -0
- package/src/utils/triggerNormalizer.ts +149 -0
- package/src/utils/validationInterceptor.ts +650 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
// src/services/flowManager.ts
|
|
2
|
+
// Service for managing contextual flows and experience indicators
|
|
3
|
+
// Supports both CSS and XPath selectors for targeting elements
|
|
4
|
+
|
|
5
|
+
import { LocationContextService } from './locationContextService';
|
|
6
|
+
import type { ModalSequencePayload } from '../experiences/types';
|
|
7
|
+
import { resolveSelector } from '../utils/selectors';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interface for flows with contextual triggering properties
|
|
11
|
+
*/
|
|
12
|
+
export interface ContextualFlow {
|
|
13
|
+
id: string;
|
|
14
|
+
type: string;
|
|
15
|
+
payload: any;
|
|
16
|
+
elementSelector?: string;
|
|
17
|
+
elementTrigger?: string;
|
|
18
|
+
elementLocation?: string;
|
|
19
|
+
originalData?: any; // Store original flow data for reference
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manager for contextual flows and experience indicators
|
|
24
|
+
*/
|
|
25
|
+
export class FlowManager {
|
|
26
|
+
private static _instance: FlowManager;
|
|
27
|
+
private _flows: ContextualFlow[] = [];
|
|
28
|
+
private _pendingFlows: ContextualFlow[] = []; // Flows with selectors not found in current page
|
|
29
|
+
private _locationService = LocationContextService.getInstance();
|
|
30
|
+
private _activeIndicators: Map<string, HTMLElement> = new Map();
|
|
31
|
+
private _activeListeners: Map<string, { element: HTMLElement, eventType: string, handler: EventListener }[]> = new Map();
|
|
32
|
+
private _indicatorStyle: HTMLStyleElement | null = null;
|
|
33
|
+
private _previousScreenId: string | undefined;
|
|
34
|
+
|
|
35
|
+
// Track active flow to prevent multiple flows from running simultaneously
|
|
36
|
+
private _activeFlowId: string | null = null;
|
|
37
|
+
private _flowInProgress: boolean = false;
|
|
38
|
+
|
|
39
|
+
// Step-level execution guards (NEW)
|
|
40
|
+
private _activeStepTriggered: boolean = false;
|
|
41
|
+
private _currentActiveStep: string | null = null;
|
|
42
|
+
|
|
43
|
+
// Track retry attempts to avoid infinite loops
|
|
44
|
+
private _retryAttempts: Map<string, number> = new Map();
|
|
45
|
+
private _maxRetryAttempts: number = 5; // Maximum number of retries per flow
|
|
46
|
+
private _domObserver: MutationObserver | null = null;
|
|
47
|
+
private _domChangeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
|
|
49
|
+
private constructor() {
|
|
50
|
+
console.debug('[DAP] Initializing FlowManager');
|
|
51
|
+
|
|
52
|
+
// Subscribe to location changes
|
|
53
|
+
this._locationService.subscribe(this.handleLocationChange.bind(this));
|
|
54
|
+
|
|
55
|
+
// Add indicator styles
|
|
56
|
+
this.injectIndicatorStyles();
|
|
57
|
+
console.debug('[DAP] Indicator styles injected');
|
|
58
|
+
|
|
59
|
+
// Ensure we re-evaluate flows when DOM is fully ready
|
|
60
|
+
if (document.readyState !== 'complete') {
|
|
61
|
+
console.debug('[DAP] Document not complete, adding load event listener');
|
|
62
|
+
window.addEventListener('load', () => {
|
|
63
|
+
console.debug('[DAP] Window fully loaded, refreshing flow visibility');
|
|
64
|
+
this.refreshFlowVisibility(true);
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
console.debug('[DAP] Document already complete');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Set up DOM observer for dynamic content changes
|
|
71
|
+
this.setupDomObserver();
|
|
72
|
+
|
|
73
|
+
console.debug('[DAP] FlowManager initialized');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set up an observer to watch for DOM changes that might affect flow indicators
|
|
78
|
+
*/
|
|
79
|
+
private setupDomObserver(): void {
|
|
80
|
+
try {
|
|
81
|
+
this._domObserver = new MutationObserver((mutations) => {
|
|
82
|
+
// Only process significant DOM changes when we have pending flows
|
|
83
|
+
if (this._pendingFlows.length === 0) return;
|
|
84
|
+
|
|
85
|
+
let significantChanges = false;
|
|
86
|
+
|
|
87
|
+
// Check if these mutations are significant enough to re-evaluate flows
|
|
88
|
+
for (const mutation of mutations) {
|
|
89
|
+
// Added or removed nodes might affect our selectors
|
|
90
|
+
if (mutation.type === 'childList' &&
|
|
91
|
+
(mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
|
|
92
|
+
significantChanges = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (significantChanges) {
|
|
98
|
+
// Debounce the refresh to avoid excessive processing
|
|
99
|
+
if (this._domChangeTimer !== null) {
|
|
100
|
+
clearTimeout(this._domChangeTimer);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this._domChangeTimer = setTimeout(() => {
|
|
104
|
+
console.debug('[DAP] Significant DOM changes detected, rechecking pending flows');
|
|
105
|
+
this.checkPendingFlows();
|
|
106
|
+
}, 200);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Start observing the document body
|
|
111
|
+
if (document.body) {
|
|
112
|
+
this._domObserver.observe(document.body, {
|
|
113
|
+
childList: true,
|
|
114
|
+
subtree: true
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.warn('[DAP] Unable to setup DOM observer:', err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if any pending flows can now be displayed after DOM changes
|
|
124
|
+
*/
|
|
125
|
+
private checkPendingFlows(): void {
|
|
126
|
+
if (this._pendingFlows.length === 0) return;
|
|
127
|
+
|
|
128
|
+
console.debug(`[DAP] Checking ${this._pendingFlows.length} pending flows after DOM changes`);
|
|
129
|
+
|
|
130
|
+
// Copy the array to avoid modification issues during iteration
|
|
131
|
+
const pendingFlows = [...this._pendingFlows];
|
|
132
|
+
|
|
133
|
+
// Clear the pending flows list before processing
|
|
134
|
+
this._pendingFlows = [];
|
|
135
|
+
|
|
136
|
+
// Try to display each pending flow with the updated DOM
|
|
137
|
+
pendingFlows.forEach(flow => {
|
|
138
|
+
console.debug(`[DAP] Rechecking flow after DOM changes: ${flow.id}`);
|
|
139
|
+
this.evaluateFlowVisibility(flow);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the singleton instance of the FlowManager
|
|
145
|
+
*/
|
|
146
|
+
public static getInstance(): FlowManager {
|
|
147
|
+
if (!this._instance) {
|
|
148
|
+
this._instance = new FlowManager();
|
|
149
|
+
}
|
|
150
|
+
return this._instance;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Register a single flow with the manager
|
|
155
|
+
* @param flow The flow to register
|
|
156
|
+
*/
|
|
157
|
+
public registerFlow(flow: ContextualFlow): void {
|
|
158
|
+
this._flows.push(flow);
|
|
159
|
+
this.evaluateFlowVisibility(flow);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Register multiple flows with the manager
|
|
164
|
+
* @param flows The flows to register
|
|
165
|
+
*/
|
|
166
|
+
public registerFlows(flows: ContextualFlow[]): void {
|
|
167
|
+
this._flows.push(...flows);
|
|
168
|
+
this.refreshFlowVisibility();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get flows that match the specified context
|
|
173
|
+
* @param context Context to match against
|
|
174
|
+
* @returns Array of matching flows
|
|
175
|
+
*/
|
|
176
|
+
public getFlowsForContext(context: { screenId?: string; elementSelector?: string }): ContextualFlow[] {
|
|
177
|
+
return this._flows.filter(flow => {
|
|
178
|
+
// Match by screen ID if specified, normalizing paths by removing leading slashes
|
|
179
|
+
if (context.screenId && flow.elementLocation) {
|
|
180
|
+
const normalizedContextId = context.screenId.replace(/^\/+/, '');
|
|
181
|
+
const normalizedFlowLocation = flow.elementLocation.replace(/^\/+/, '');
|
|
182
|
+
|
|
183
|
+
if (normalizedFlowLocation !== normalizedContextId && normalizedFlowLocation !== '*') {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Match by element selector if specified
|
|
189
|
+
if (context.elementSelector && flow.elementSelector) {
|
|
190
|
+
return flow.elementSelector === context.elementSelector;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get all registered flows
|
|
199
|
+
*/
|
|
200
|
+
public getAllFlows(): ContextualFlow[] {
|
|
201
|
+
return [...this._flows];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get all pending flows (flows with selectors not found in the current page)
|
|
206
|
+
*/
|
|
207
|
+
public getPendingFlows(): ContextualFlow[] {
|
|
208
|
+
return [...this._pendingFlows];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Clear all registered flows
|
|
213
|
+
*/
|
|
214
|
+
public clearFlows(): void {
|
|
215
|
+
this._flows = [];
|
|
216
|
+
this._pendingFlows = [];
|
|
217
|
+
this.clearAllIndicators();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Manually add a flow to the pending list
|
|
222
|
+
* @param flow The flow to mark as pending
|
|
223
|
+
*/
|
|
224
|
+
public addPendingFlow(flow: ContextualFlow): void {
|
|
225
|
+
if (!this._pendingFlows.some(pFlow => pFlow.id === flow.id)) {
|
|
226
|
+
this._pendingFlows.push(flow);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Handle location context change
|
|
232
|
+
*/
|
|
233
|
+
private handleLocationChange(context: { currentPath: string; screenId?: string }): void {
|
|
234
|
+
console.debug('[DAP] Location context changed:', context);
|
|
235
|
+
|
|
236
|
+
// Store the previous screen ID for context tracking
|
|
237
|
+
this._previousScreenId = context.screenId;
|
|
238
|
+
|
|
239
|
+
// Clear all current indicators since we're potentially on a new page
|
|
240
|
+
this.clearAllIndicators();
|
|
241
|
+
|
|
242
|
+
// Give the DOM a moment to update before evaluating flows
|
|
243
|
+
// This helps with single page applications where route changes don't fully reload the page
|
|
244
|
+
setTimeout(() => {
|
|
245
|
+
console.debug(`[DAP] Evaluating flows for new location context`);
|
|
246
|
+
|
|
247
|
+
// Re-evaluate all flows for the new context (both registered and pending)
|
|
248
|
+
const allFlows = [...this._flows, ...this._pendingFlows];
|
|
249
|
+
|
|
250
|
+
// Clear pending flows before re-evaluation
|
|
251
|
+
this._pendingFlows = [];
|
|
252
|
+
|
|
253
|
+
// Process each flow for the new location
|
|
254
|
+
allFlows.forEach(flow => {
|
|
255
|
+
console.debug(`[DAP] Re-evaluating flow for new location: ${flow.id}`);
|
|
256
|
+
this.evaluateFlowVisibility(flow);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
console.debug(`[DAP] Location change evaluation complete. Pending flows: ${this._pendingFlows.length}`);
|
|
260
|
+
}, 100); // Small delay to allow DOM updates
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Refresh visibility for all registered flows
|
|
265
|
+
* @param forceRetry Force retry even for indicators that were previously not found
|
|
266
|
+
*/
|
|
267
|
+
private refreshFlowVisibility(forceRetry: boolean = false): void {
|
|
268
|
+
// Remove all indicators
|
|
269
|
+
this.clearAllIndicators();
|
|
270
|
+
|
|
271
|
+
// Reset retry counters if forcing retry
|
|
272
|
+
if (forceRetry) {
|
|
273
|
+
this._retryAttempts.clear();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Re-evaluate all flows for the current context
|
|
277
|
+
this._flows.forEach(flow => this.evaluateFlowVisibility(flow));
|
|
278
|
+
|
|
279
|
+
// If we want to force retry for all flows that weren't found
|
|
280
|
+
if (forceRetry && document.readyState === 'complete') {
|
|
281
|
+
// Schedule another check after everything is loaded
|
|
282
|
+
setTimeout(() => {
|
|
283
|
+
console.debug('[DAP] Running secondary flow visibility check after page load');
|
|
284
|
+
|
|
285
|
+
// Try again for any flows that might have been missed
|
|
286
|
+
if (this._pendingFlows.length > 0) {
|
|
287
|
+
const pendingFlows = [...this._pendingFlows];
|
|
288
|
+
this._pendingFlows = [];
|
|
289
|
+
|
|
290
|
+
pendingFlows.forEach(flow => {
|
|
291
|
+
console.debug(`[DAP] Re-attempting to display pending flow: ${flow.id}`);
|
|
292
|
+
this.evaluateFlowVisibility(flow);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}, 500);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Evaluate whether a flow should be visible in the current context
|
|
301
|
+
*/
|
|
302
|
+
private evaluateFlowVisibility(flow: ContextualFlow): void {
|
|
303
|
+
const currentContext = this._locationService.getContext();
|
|
304
|
+
|
|
305
|
+
console.debug(`[DAP] Evaluating flow visibility:`, {
|
|
306
|
+
flowId: flow.id,
|
|
307
|
+
elementSelector: flow.elementSelector,
|
|
308
|
+
elementLocation: flow.elementLocation,
|
|
309
|
+
currentLocation: currentContext
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Handle flows without element selectors (immediate trigger flows)
|
|
313
|
+
if (!flow.elementSelector) {
|
|
314
|
+
console.debug(`[DAP] Flow ${flow.id} has no elementSelector - checking location constraints`);
|
|
315
|
+
|
|
316
|
+
// Check location constraints for immediate flows
|
|
317
|
+
if (this.matchesLocationConstraint(flow, currentContext)) {
|
|
318
|
+
console.debug(`[DAP] Immediate flow ${flow.id} matches location, triggering`);
|
|
319
|
+
// Trigger immediately if location matches
|
|
320
|
+
this.triggerFlow(flow);
|
|
321
|
+
} else {
|
|
322
|
+
console.debug(`[DAP] Immediate flow ${flow.id} doesn't match location, adding to pending`);
|
|
323
|
+
this.addToPendingIfNotExists(flow);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Handle flows with element selectors
|
|
329
|
+
const matchesLocation = this.matchesLocationConstraint(flow, currentContext);
|
|
330
|
+
|
|
331
|
+
if (matchesLocation) {
|
|
332
|
+
console.debug(`[DAP] Flow ${flow.id} matches location, attempting to add indicator`);
|
|
333
|
+
// Try to add the flow indicator
|
|
334
|
+
const wasAdded = this.addFlowIndicator(flow);
|
|
335
|
+
|
|
336
|
+
// If the flow couldn't be added (selector not found),
|
|
337
|
+
// add it to pending flows for when the element appears
|
|
338
|
+
if (!wasAdded) {
|
|
339
|
+
console.debug(`[DAP] Indicator not added for flow ${flow.id}, adding to pending`);
|
|
340
|
+
this.addToPendingIfNotExists(flow);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
console.debug(`[DAP] Flow ${flow.id} doesn't match current location, adding to pending for future pages`);
|
|
344
|
+
// Flow doesn't match current location - add to pending for other pages
|
|
345
|
+
this.addToPendingIfNotExists(flow);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if flow matches location constraint
|
|
351
|
+
*/
|
|
352
|
+
private matchesLocationConstraint(flow: ContextualFlow, currentContext: any): boolean {
|
|
353
|
+
// If flow has no location constraint, always consider it for current screen
|
|
354
|
+
if (!flow.elementLocation) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Normalize paths by removing leading forward slashes
|
|
359
|
+
const normalizedFlowLocation = flow.elementLocation.replace(/^\/+/, '');
|
|
360
|
+
const normalizedScreenId = currentContext.screenId ? currentContext.screenId.replace(/^\/+/, '') : '';
|
|
361
|
+
const normalizedCurrentPath = currentContext.currentPath ? currentContext.currentPath.replace(/^\/+/, '') : '';
|
|
362
|
+
|
|
363
|
+
// Check if flow should be visible on current screen
|
|
364
|
+
return normalizedFlowLocation === normalizedScreenId ||
|
|
365
|
+
normalizedFlowLocation === '*' ||
|
|
366
|
+
normalizedFlowLocation === normalizedCurrentPath;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Add flow to pending list if it doesn't already exist
|
|
371
|
+
*/
|
|
372
|
+
private addToPendingIfNotExists(flow: ContextualFlow): void {
|
|
373
|
+
if (!this._pendingFlows.some(pFlow => pFlow.id === flow.id)) {
|
|
374
|
+
this._pendingFlows.push(flow);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Add a visual indicator for an available flow
|
|
380
|
+
* @returns boolean indicating whether the indicator was successfully added
|
|
381
|
+
*/
|
|
382
|
+
private addFlowIndicator(flow: ContextualFlow): boolean {
|
|
383
|
+
if (!flow.elementSelector) return false;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// Check if the selector looks like XPath
|
|
387
|
+
const isXPath = flow.elementSelector.startsWith('/') ||
|
|
388
|
+
flow.elementSelector.startsWith('./') ||
|
|
389
|
+
flow.elementSelector.startsWith('//') ||
|
|
390
|
+
flow.elementSelector.includes('@') ||
|
|
391
|
+
/\[\d+\]/.test(flow.elementSelector);
|
|
392
|
+
|
|
393
|
+
if (isXPath) {
|
|
394
|
+
console.debug(`[DAP] Using XPath selector: ${flow.elementSelector}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Use resolveSelector instead of document.querySelector to support XPath
|
|
398
|
+
const element = resolveSelector(flow.elementSelector);
|
|
399
|
+
if (!element) {
|
|
400
|
+
console.debug(`[DAP] Element not found for selector: ${flow.elementSelector}`);
|
|
401
|
+
|
|
402
|
+
// Page is still loading or element might be part of dynamic content
|
|
403
|
+
// Use retry logic instead of immediately failing
|
|
404
|
+
if (!flow.id) {
|
|
405
|
+
console.debug(`[DAP] Cannot schedule retry for flow without ID`);
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Get current retry count
|
|
410
|
+
const retryCount = this._retryAttempts.get(flow.id) || 0;
|
|
411
|
+
|
|
412
|
+
// Check if we've hit the max retry count
|
|
413
|
+
if (retryCount >= this._maxRetryAttempts) {
|
|
414
|
+
console.debug(`[DAP] Maximum retry attempts (${this._maxRetryAttempts}) reached for flow: ${flow.id}`);
|
|
415
|
+
this._retryAttempts.delete(flow.id); // Reset for future attempts
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Schedule a retry with exponential backoff
|
|
420
|
+
const delay = Math.min(300 * Math.pow(2, retryCount), 5000); // Cap at 5 seconds
|
|
421
|
+
console.debug(`[DAP] Scheduling retry ${retryCount + 1}/${this._maxRetryAttempts} in ${delay}ms for flow: ${flow.id}`);
|
|
422
|
+
|
|
423
|
+
// Update retry counter
|
|
424
|
+
this._retryAttempts.set(flow.id, retryCount + 1);
|
|
425
|
+
|
|
426
|
+
// Schedule retry
|
|
427
|
+
setTimeout(() => {
|
|
428
|
+
// Try again
|
|
429
|
+
const retryResult = this.addFlowIndicator(flow);
|
|
430
|
+
|
|
431
|
+
// If we've successfully added the flow on retry, remove it from pending
|
|
432
|
+
if (retryResult && flow.id && this._pendingFlows.some(pFlow => pFlow.id === flow.id)) {
|
|
433
|
+
this._pendingFlows = this._pendingFlows.filter(pFlow => pFlow.id !== flow.id);
|
|
434
|
+
}
|
|
435
|
+
}, delay);
|
|
436
|
+
|
|
437
|
+
// Return true so we don't immediately mark as pending
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Create indicator if it doesn't exist for this element
|
|
442
|
+
if (!this._activeIndicators.has(flow.elementSelector)) {
|
|
443
|
+
// Get the parent element position and style
|
|
444
|
+
const targetElement = element as HTMLElement;
|
|
445
|
+
|
|
446
|
+
// Create container for positioning - this will be relative to the parent
|
|
447
|
+
const container = document.createElement('div');
|
|
448
|
+
container.className = 'dap-indicator-container';
|
|
449
|
+
container.style.position = 'relative';
|
|
450
|
+
container.style.zIndex = '9999';
|
|
451
|
+
container.style.width = '0';
|
|
452
|
+
container.style.height = '0';
|
|
453
|
+
container.style.overflow = 'visible';
|
|
454
|
+
|
|
455
|
+
// Create the indicator
|
|
456
|
+
const indicator = document.createElement('div');
|
|
457
|
+
indicator.className = 'dap-experience-indicator';
|
|
458
|
+
indicator.setAttribute('role', 'button');
|
|
459
|
+
indicator.setAttribute('aria-label', 'View available content');
|
|
460
|
+
indicator.setAttribute('tabindex', '0');
|
|
461
|
+
indicator.dataset.selector = flow.elementSelector;
|
|
462
|
+
|
|
463
|
+
// SVG light bulb icon (beautified)
|
|
464
|
+
indicator.innerHTML = `
|
|
465
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
466
|
+
<path d="M12 2C7.58172 2 4 5.58172 4 10C4 12.8492 5.62545 15.3373 8 16.584V19C8 19.5523 8.44772 20 9 20H15C15.5523 20 16 19.5523 16 19V16.584C18.3745 15.3373 20 12.8492 20 10C20 5.58172 16.4183 2 12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
|
467
|
+
<path d="M10 22H14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
468
|
+
</svg>
|
|
469
|
+
`;
|
|
470
|
+
|
|
471
|
+
// Add the indicator to the container
|
|
472
|
+
container.appendChild(indicator);
|
|
473
|
+
|
|
474
|
+
// The magic: insert the container directly after the target element in the DOM
|
|
475
|
+
// This ensures the indicator will always follow its parent during scroll/layout changes
|
|
476
|
+
if (targetElement.parentNode) {
|
|
477
|
+
targetElement.parentNode.insertBefore(container, targetElement.nextSibling);
|
|
478
|
+
console.debug(`[DAP] Added indicator container to DOM after target element`);
|
|
479
|
+
|
|
480
|
+
// Position the indicator relative to its parent
|
|
481
|
+
this.positionIndicatorRelative(indicator, targetElement);
|
|
482
|
+
|
|
483
|
+
// Watch for resize changes to reposition
|
|
484
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
485
|
+
this.positionIndicatorRelative(indicator, targetElement);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
resizeObserver.observe(targetElement);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.warn('[DAP] ResizeObserver not supported, indicator may not reposition correctly');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Add mutation observer to detect style/attribute changes
|
|
495
|
+
try {
|
|
496
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
497
|
+
// Only check for style or class changes that might affect positioning
|
|
498
|
+
const shouldUpdate = mutations.some(m =>
|
|
499
|
+
m.type === 'attributes' &&
|
|
500
|
+
(m.attributeName === 'style' || m.attributeName === 'class')
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (shouldUpdate) {
|
|
504
|
+
this.positionIndicatorRelative(indicator, targetElement);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
mutationObserver.observe(targetElement, {
|
|
509
|
+
attributes: true,
|
|
510
|
+
attributeFilter: ['style', 'class']
|
|
511
|
+
});
|
|
512
|
+
} catch (err) {
|
|
513
|
+
console.warn('[DAP] MutationObserver not supported, indicator may not update on style changes');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Attach event listeners for the flow
|
|
517
|
+
this.attachTriggerHandler(indicator, targetElement, flow);
|
|
518
|
+
|
|
519
|
+
// Store the container for later cleanup
|
|
520
|
+
this._activeIndicators.set(flow.elementSelector, container);
|
|
521
|
+
|
|
522
|
+
// Add window resize handler to ensure positioning stays correct
|
|
523
|
+
const windowResizeHandler = () => this.positionIndicatorRelative(indicator, targetElement);
|
|
524
|
+
window.addEventListener('resize', windowResizeHandler);
|
|
525
|
+
|
|
526
|
+
if (!this._activeListeners.has('resize')) {
|
|
527
|
+
this._activeListeners.set('resize', []);
|
|
528
|
+
}
|
|
529
|
+
this._activeListeners.get('resize')!.push({
|
|
530
|
+
element: window as any as HTMLElement,
|
|
531
|
+
eventType: 'resize',
|
|
532
|
+
handler: windowResizeHandler
|
|
533
|
+
});
|
|
534
|
+
} else {
|
|
535
|
+
console.warn(`[DAP] Target element has no parent, cannot attach indicator`);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
console.debug(`[DAP] Added indicator for flow: ${flow.id} at selector: ${flow.elementSelector}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Successfully added indicator
|
|
543
|
+
return true;
|
|
544
|
+
} catch (err) {
|
|
545
|
+
console.error('[DAP] Error adding flow indicator:', err);
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// This method has been incorporated directly into addFlowIndicator
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Position indicator directly relative to its target element
|
|
554
|
+
* This uses absolute positioning relative to the target's box model
|
|
555
|
+
*/
|
|
556
|
+
private positionIndicatorRelative(indicator: HTMLElement, targetElement: HTMLElement): void {
|
|
557
|
+
// Skip if elements are no longer in the DOM
|
|
558
|
+
if (!document.body.contains(indicator) || !document.body.contains(targetElement)) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Use requestAnimationFrame for smoother positioning
|
|
563
|
+
requestAnimationFrame(() => {
|
|
564
|
+
try {
|
|
565
|
+
// Get target element dimensions
|
|
566
|
+
const rect = targetElement.getBoundingClientRect();
|
|
567
|
+
const indicatorWidth = indicator.offsetWidth || 36;
|
|
568
|
+
const indicatorHeight = indicator.offsetHeight || 36;
|
|
569
|
+
|
|
570
|
+
// Position at the top-right corner of the target element
|
|
571
|
+
indicator.style.position = 'absolute';
|
|
572
|
+
indicator.style.top = `-${indicatorHeight / 2}px`;
|
|
573
|
+
indicator.style.left = `${rect.width - (indicatorWidth / 2)}px`;
|
|
574
|
+
indicator.style.zIndex = '9999';
|
|
575
|
+
|
|
576
|
+
// Add debug logging
|
|
577
|
+
console.debug(`[DAP] Positioned indicator relative to target element`);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.error('[DAP] Error positioning indicator:', err);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Attach event handlers based on trigger type
|
|
586
|
+
*/
|
|
587
|
+
private attachTriggerHandler(
|
|
588
|
+
indicator: HTMLElement,
|
|
589
|
+
targetElement: HTMLElement,
|
|
590
|
+
flow: ContextualFlow
|
|
591
|
+
): void {
|
|
592
|
+
const trigger = flow.elementTrigger?.toLowerCase() || '';
|
|
593
|
+
let eventType: string;
|
|
594
|
+
|
|
595
|
+
// Map trigger to event type
|
|
596
|
+
if (trigger.includes('click')) {
|
|
597
|
+
eventType = 'click';
|
|
598
|
+
} else if (trigger.includes('hover')) {
|
|
599
|
+
eventType = 'mouseenter';
|
|
600
|
+
} else if (trigger.includes('focus')) {
|
|
601
|
+
eventType = 'focus';
|
|
602
|
+
} else {
|
|
603
|
+
// Default to click
|
|
604
|
+
eventType = 'click';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Attach event listener to both indicator and target if needed
|
|
608
|
+
const triggerHandler = () => this.triggerFlow(flow);
|
|
609
|
+
|
|
610
|
+
// Indicator always uses click and keydown (for accessibility)
|
|
611
|
+
indicator.addEventListener('click', triggerHandler);
|
|
612
|
+
indicator.addEventListener('keydown', (e) => {
|
|
613
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
triggerHandler();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// For hover/focus triggers, also attach to the target element
|
|
620
|
+
if (eventType !== 'click') {
|
|
621
|
+
targetElement.addEventListener(eventType, triggerHandler);
|
|
622
|
+
|
|
623
|
+
// Track this additional handler for cleanup
|
|
624
|
+
if (!this._activeListeners.has(flow.elementSelector!)) {
|
|
625
|
+
this._activeListeners.set(flow.elementSelector!, []);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
this._activeListeners.get(flow.elementSelector!)!.push({
|
|
629
|
+
element: targetElement,
|
|
630
|
+
eventType,
|
|
631
|
+
handler: triggerHandler
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Trigger a flow
|
|
638
|
+
*/
|
|
639
|
+
private triggerFlow(flow: ContextualFlow): void {
|
|
640
|
+
console.debug(`[DAP] triggerFlow called for flow: ${flow.id}, _flowInProgress: ${this._flowInProgress}`);
|
|
641
|
+
|
|
642
|
+
// Special handling for flows without elementSelector (immediate flows)
|
|
643
|
+
if (!flow.elementSelector) {
|
|
644
|
+
console.debug(`[DAP] Triggering immediate flow (no elementSelector): ${flow.id}`);
|
|
645
|
+
this.renderFlow(flow, true); // Always treat as new flow
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// If no active flow, this is a new flow start
|
|
650
|
+
if (!this._flowInProgress) {
|
|
651
|
+
this.startNewFlow(flow);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// If a flow is already active, this should be a step trigger within that flow
|
|
656
|
+
// Check if this trigger belongs to the current active step
|
|
657
|
+
const stepId = this.extractStepIdFromFlow(flow);
|
|
658
|
+
|
|
659
|
+
if (!stepId) {
|
|
660
|
+
console.debug(`[DAP] Ignoring trigger - no step ID found for flow: ${flow.id}`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Check if this is the current active step
|
|
665
|
+
if (this._currentActiveStep && this._currentActiveStep !== stepId) {
|
|
666
|
+
console.debug(`[DAP] Trigger ignored - not for current active step. Current: ${this._currentActiveStep}, Trigger: ${stepId}`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Check if current step has already been triggered
|
|
671
|
+
if (this._activeStepTriggered) {
|
|
672
|
+
console.debug(`[DAP] Trigger ignored because step already triggered: ${stepId}`);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Valid step trigger - execute it
|
|
677
|
+
this.executeStepTrigger(flow, stepId);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Start a new flow
|
|
682
|
+
*/
|
|
683
|
+
private startNewFlow(flow: ContextualFlow): void {
|
|
684
|
+
console.debug(`[DAP] Starting new flow: ${flow.id}, setting _flowInProgress = true`);
|
|
685
|
+
|
|
686
|
+
// Mark flow as in progress
|
|
687
|
+
this._flowInProgress = true;
|
|
688
|
+
this._activeFlowId = flow.id;
|
|
689
|
+
this._activeStepTriggered = false;
|
|
690
|
+
this._currentActiveStep = this.extractStepIdFromFlow(flow);
|
|
691
|
+
|
|
692
|
+
console.debug(`[DAP] Flow state: _flowInProgress=${this._flowInProgress}, _activeFlowId=${this._activeFlowId}`);
|
|
693
|
+
|
|
694
|
+
this.renderFlow(flow, true); // true = new flow
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Execute a step trigger within an active flow
|
|
699
|
+
*/
|
|
700
|
+
private executeStepTrigger(flow: ContextualFlow, stepId: string): void {
|
|
701
|
+
// Mark this step as triggered
|
|
702
|
+
this._activeStepTriggered = true;
|
|
703
|
+
|
|
704
|
+
console.debug(`[DAP] Executing step trigger: ${stepId} in flow: ${flow.id}`);
|
|
705
|
+
|
|
706
|
+
this.renderFlow(flow, false); // false = step trigger, not new flow
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Extract step ID from flow (helper method)
|
|
711
|
+
*/
|
|
712
|
+
private extractStepIdFromFlow(flow: ContextualFlow): string | null {
|
|
713
|
+
// Try to get step ID from payload or flow structure
|
|
714
|
+
if (flow.payload?.steps && Array.isArray(flow.payload.steps)) {
|
|
715
|
+
// For multi-step flows, get the current step ID
|
|
716
|
+
const currentStep = flow.payload.steps[0]; // Assuming first step for triggers
|
|
717
|
+
return currentStep?.stepId || null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// For single-step flows, use flow ID as step ID
|
|
721
|
+
return flow.id;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Render a flow (common logic for new flows and step triggers)
|
|
726
|
+
*/
|
|
727
|
+
private renderFlow(flow: ContextualFlow, isNewFlow: boolean): void {
|
|
728
|
+
// Import dynamically to avoid circular dependencies
|
|
729
|
+
import('../experiences/registry').then(({ getRenderer }) => {
|
|
730
|
+
const renderer = getRenderer(flow.type);
|
|
731
|
+
if (renderer) {
|
|
732
|
+
// Create a wrapped renderer that handles flow completion
|
|
733
|
+
const completionTracker = {
|
|
734
|
+
onComplete: () => {
|
|
735
|
+
console.debug(`[DAP] Flow ${flow.id} completed`);
|
|
736
|
+
this.onFlowComplete();
|
|
737
|
+
},
|
|
738
|
+
onStepAdvance: (stepId: string) => {
|
|
739
|
+
// Called when advancing to next step - reset step-level trigger state
|
|
740
|
+
console.debug(`[DAP] Advancing to step: ${stepId}`);
|
|
741
|
+
this.onStepAdvance(stepId);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// Pass the flow and completion tracker to the renderer
|
|
746
|
+
renderer({
|
|
747
|
+
id: flow.id,
|
|
748
|
+
type: flow.type,
|
|
749
|
+
payload: {
|
|
750
|
+
...flow.payload,
|
|
751
|
+
_completionTracker: completionTracker
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Reset step trigger state after successful rendering
|
|
756
|
+
// This allows subsequent step triggers to execute properly
|
|
757
|
+
if (!isNewFlow) {
|
|
758
|
+
this._activeStepTriggered = false;
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
console.warn(`[DAP] No renderer found for flow type: ${flow.type}`);
|
|
762
|
+
// Reset the flow state if no renderer found
|
|
763
|
+
if (isNewFlow) {
|
|
764
|
+
this.onFlowComplete();
|
|
765
|
+
} else {
|
|
766
|
+
// Reset step trigger state for step triggers with no renderer
|
|
767
|
+
this._activeStepTriggered = false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}).catch(err => {
|
|
771
|
+
console.error('[DAP] Error triggering flow:', err);
|
|
772
|
+
// Reset the flow state on error
|
|
773
|
+
if (isNewFlow) {
|
|
774
|
+
this.onFlowComplete();
|
|
775
|
+
} else {
|
|
776
|
+
// Reset step trigger state on error
|
|
777
|
+
this._activeStepTriggered = false;
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Handle flow completion
|
|
784
|
+
*/
|
|
785
|
+
private onFlowComplete(): void {
|
|
786
|
+
console.debug(`[DAP] Flow completed: ${this._activeFlowId}, setting _flowInProgress = false`);
|
|
787
|
+
this._flowInProgress = false;
|
|
788
|
+
this._activeFlowId = null;
|
|
789
|
+
this._activeStepTriggered = false;
|
|
790
|
+
this._currentActiveStep = null;
|
|
791
|
+
console.debug(`[DAP] Flow state reset: _flowInProgress=${this._flowInProgress}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Handle step advancement
|
|
796
|
+
*/
|
|
797
|
+
private onStepAdvance(stepId: string): void {
|
|
798
|
+
this._activeStepTriggered = false; // Reset for new step
|
|
799
|
+
this._currentActiveStep = stepId;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Clear all indicators and event listeners
|
|
804
|
+
*/
|
|
805
|
+
private clearAllIndicators(): void {
|
|
806
|
+
// Remove all indicator container elements
|
|
807
|
+
this._activeIndicators.forEach((element, key) => {
|
|
808
|
+
if (element.tagName) {
|
|
809
|
+
// Remove any type of container/indicator element
|
|
810
|
+
if (element.className === 'dap-indicator-container' ||
|
|
811
|
+
element.className === 'dap-indicator-wrapper' ||
|
|
812
|
+
element.className === 'dap-experience-indicator') {
|
|
813
|
+
element.remove();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
this._activeIndicators.clear();
|
|
818
|
+
|
|
819
|
+
// Remove all attached event listeners
|
|
820
|
+
this._activeListeners.forEach((listeners, selector) => {
|
|
821
|
+
listeners.forEach(({ element, eventType, handler }) => {
|
|
822
|
+
element.removeEventListener(eventType, handler);
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
this._activeListeners.clear();
|
|
826
|
+
|
|
827
|
+
console.debug('[DAP] Cleared all indicators and event listeners');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Inject indicator styles into the document
|
|
832
|
+
*/
|
|
833
|
+
private injectIndicatorStyles(): void {
|
|
834
|
+
if (this._indicatorStyle) return;
|
|
835
|
+
|
|
836
|
+
this._indicatorStyle = document.createElement('style');
|
|
837
|
+
this._indicatorStyle.id = 'dap-experience-indicator-style';
|
|
838
|
+
this._indicatorStyle.type = 'text/css';
|
|
839
|
+
this._indicatorStyle.textContent = `
|
|
840
|
+
.dap-indicator-container {
|
|
841
|
+
pointer-events: none;
|
|
842
|
+
z-index: 9999;
|
|
843
|
+
position: relative;
|
|
844
|
+
display: block;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.dap-experience-indicator {
|
|
848
|
+
width: 36px;
|
|
849
|
+
height: 36px;
|
|
850
|
+
cursor: pointer;
|
|
851
|
+
background-color: rgba(255, 196, 0, 0.95); /* Yellow color */
|
|
852
|
+
border-radius: 50%;
|
|
853
|
+
padding: 6px;
|
|
854
|
+
display: flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
justify-content: center;
|
|
857
|
+
color: #000000;
|
|
858
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
|
859
|
+
transition: all 0.2s ease-in-out;
|
|
860
|
+
animation: dap-indicator-pulse 2s infinite;
|
|
861
|
+
pointer-events: auto;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.dap-experience-indicator:hover,
|
|
865
|
+
.dap-experience-indicator:focus {
|
|
866
|
+
transform: scale(1.1);
|
|
867
|
+
background-color: rgba(255, 196, 0, 1);
|
|
868
|
+
outline: none;
|
|
869
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.dap-experience-indicator svg {
|
|
873
|
+
width: 20px;
|
|
874
|
+
height: 20px;
|
|
875
|
+
fill: none;
|
|
876
|
+
stroke: currentColor;
|
|
877
|
+
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.2));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
@keyframes dap-indicator-pulse {
|
|
881
|
+
0% { box-shadow: 0 0 0 0 rgba(255, 196, 0, 0.4); }
|
|
882
|
+
70% { box-shadow: 0 0 0 8px rgba(255, 196, 0, 0); }
|
|
883
|
+
100% { box-shadow: 0 0 0 0 rgba(255, 196, 0, 0); }
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
@media (max-width: 768px) {
|
|
887
|
+
.dap-experience-indicator {
|
|
888
|
+
width: 42px;
|
|
889
|
+
height: 42px;
|
|
890
|
+
padding: 8px;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.dap-experience-indicator svg {
|
|
894
|
+
width: 24px;
|
|
895
|
+
height: 24px;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
`;
|
|
899
|
+
|
|
900
|
+
document.head.appendChild(this._indicatorStyle);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Export singleton instance
|
|
905
|
+
export const flowManager = FlowManager.getInstance();
|