cuoral-ionic 0.0.8 → 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/CuoralIonic.podspec +17 -0
- package/README.md +136 -6
- package/dist/cuoral.d.ts +1 -0
- package/dist/cuoral.d.ts.map +1 -1
- package/dist/cuoral.js +12 -5
- package/dist/index.esm.js +219 -18
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +219 -18
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +8 -0
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +201 -8
- package/dist/modal.d.ts +2 -1
- package/dist/modal.d.ts.map +1 -1
- package/dist/modal.js +6 -5
- package/package.json +3 -2
- package/src/cuoral.ts +16 -5
- package/src/intelligence.ts +231 -8
- package/src/modal.ts +7 -5
package/dist/modal.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* Handles full-screen modal display with floating button
|
|
4
4
|
*/
|
|
5
5
|
export class CuoralModal {
|
|
6
|
-
constructor(widgetUrl, showFloatingButton = true) {
|
|
6
|
+
constructor(widgetUrl, showFloatingButton = true, primaryColor = '#007AFF') {
|
|
7
7
|
this.isOpen = false;
|
|
8
8
|
this.widgetUrl = widgetUrl;
|
|
9
9
|
this.showFloatingButton = showFloatingButton;
|
|
10
|
+
this.primaryColor = primaryColor;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Update the widget URL (e.g., to include new session_id)
|
|
@@ -74,12 +75,12 @@ export class CuoralModal {
|
|
|
74
75
|
width: '56px',
|
|
75
76
|
height: '56px',
|
|
76
77
|
borderRadius: '50%',
|
|
77
|
-
backgroundColor:
|
|
78
|
+
backgroundColor: this.primaryColor,
|
|
78
79
|
display: 'flex',
|
|
79
80
|
alignItems: 'center',
|
|
80
81
|
justifyContent: 'center',
|
|
81
82
|
cursor: 'pointer',
|
|
82
|
-
boxShadow:
|
|
83
|
+
boxShadow: `0 4px 12px ${this.primaryColor}66`, // 40% opacity
|
|
83
84
|
zIndex: '999999',
|
|
84
85
|
transition: 'transform 0.2s, box-shadow 0.2s'
|
|
85
86
|
});
|
|
@@ -87,13 +88,13 @@ export class CuoralModal {
|
|
|
87
88
|
this.floatingButton.addEventListener('mouseenter', () => {
|
|
88
89
|
if (this.floatingButton) {
|
|
89
90
|
this.floatingButton.style.transform = 'scale(1.1)';
|
|
90
|
-
this.floatingButton.style.boxShadow =
|
|
91
|
+
this.floatingButton.style.boxShadow = `0 6px 16px ${this.primaryColor}80`; // 50% opacity
|
|
91
92
|
}
|
|
92
93
|
});
|
|
93
94
|
this.floatingButton.addEventListener('mouseleave', () => {
|
|
94
95
|
if (this.floatingButton) {
|
|
95
96
|
this.floatingButton.style.transform = 'scale(1)';
|
|
96
|
-
this.floatingButton.style.boxShadow =
|
|
97
|
+
this.floatingButton.style.boxShadow = `0 4px 12px ${this.primaryColor}66`; // 40% opacity
|
|
97
98
|
}
|
|
98
99
|
});
|
|
99
100
|
// Click to open modal
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cuoral-ionic",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Cuoral Ionic Framework Library -
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cuoral Ionic Framework Library - silent churn prevention platform for Ionic apps. Provides screen recording, user interaction capture, and support widget integration for enhanced customer support experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.esm.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"assets",
|
|
13
13
|
"ios",
|
|
14
14
|
"android",
|
|
15
|
+
"CuoralIonic.podspec",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
package/src/cuoral.ts
CHANGED
|
@@ -14,10 +14,14 @@ export interface CuoralOptions {
|
|
|
14
14
|
widgetBaseUrl?: string; // Allow custom widget URL
|
|
15
15
|
showFloatingButton?: boolean; // Show floating chat button (default: true)
|
|
16
16
|
useModal?: boolean; // Use modal display mode (default: true)
|
|
17
|
+
primaryColor?: string; // Custom primary color (fetched from backend if not provided)
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
interface SessionConfiguration {
|
|
20
21
|
customer_intelligence?: boolean;
|
|
22
|
+
color?: string; // Organization's primary brand color
|
|
23
|
+
config_name?: string;
|
|
24
|
+
support_name?: string;
|
|
21
25
|
[key: string]: any;
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -77,10 +81,7 @@ export class Cuoral {
|
|
|
77
81
|
|
|
78
82
|
const widgetUrl = `${baseUrl}?${params.toString()}`;
|
|
79
83
|
|
|
80
|
-
//
|
|
81
|
-
if (this.options.useModal) {
|
|
82
|
-
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
|
|
83
|
-
}
|
|
84
|
+
// Note: Modal will be created in initialize() after fetching primary color from backend
|
|
84
85
|
|
|
85
86
|
// Initialize bridge and recorder
|
|
86
87
|
this.bridge = new CuoralBridge({
|
|
@@ -106,6 +107,7 @@ export class Cuoral {
|
|
|
106
107
|
*/
|
|
107
108
|
public async initialize(): Promise<void> {
|
|
108
109
|
// Fetch session configuration and initialize intelligence if enabled by backend
|
|
110
|
+
// This also fetches the organization's primary color
|
|
109
111
|
await this.initializeIntelligence();
|
|
110
112
|
|
|
111
113
|
console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
|
|
@@ -118,7 +120,7 @@ export class Cuoral {
|
|
|
118
120
|
// Recreate modal if it was destroyed (e.g., after clearSession)
|
|
119
121
|
if (this.options.useModal && !this.modal) {
|
|
120
122
|
const widgetUrl = this.getWidgetUrl();
|
|
121
|
-
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
|
|
123
|
+
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton, this.options.primaryColor);
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
// Update modal URL with session ID
|
|
@@ -148,6 +150,15 @@ export class Cuoral {
|
|
|
148
150
|
// Fetch session configuration
|
|
149
151
|
const config = await this.fetchSessionConfiguration(sessionId);
|
|
150
152
|
|
|
153
|
+
// Extract organization's primary color from config if available
|
|
154
|
+
if (!this.options.primaryColor && config?.color) {
|
|
155
|
+
this.options.primaryColor = config.color;
|
|
156
|
+
console.log('[Cuoral] Using organization primary color:', this.options.primaryColor);
|
|
157
|
+
} else if (!this.options.primaryColor) {
|
|
158
|
+
this.options.primaryColor = '#007AFF';
|
|
159
|
+
console.log('[Cuoral] Using default primary color');
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
// If session was invalid/expired, try to initiate a new one
|
|
152
163
|
if (!config && !localStorage.getItem('__x_loadID')) {
|
|
153
164
|
sessionId = await this.initiateSession();
|
package/src/intelligence.ts
CHANGED
|
@@ -107,11 +107,13 @@ export class CuoralIntelligence {
|
|
|
107
107
|
// Session replay state
|
|
108
108
|
private rrwebStopFn: any = null;
|
|
109
109
|
private rrwebEvents: any[] = [];
|
|
110
|
+
private rrwebEmit: ((event: any) => void) | null = null; // Store emit function for custom events
|
|
110
111
|
private customEvents: CustomEventData[] = [];
|
|
111
112
|
private sessionReplayTimer: any = null;
|
|
112
113
|
private clickTimestamps: Map<string, number[]> = new Map();
|
|
113
114
|
private rageClickThreshold = 5; // 5 rapid clicks
|
|
114
115
|
private rageClickWindowMs = 2000; // Within 2 seconds
|
|
116
|
+
private shadowDOMNodeMap: Map<Element, number> = new Map(); // Map elements to unique IDs
|
|
115
117
|
|
|
116
118
|
constructor(sessionId: string) {
|
|
117
119
|
this.sessionId = sessionId;
|
|
@@ -677,6 +679,7 @@ export class CuoralIntelligence {
|
|
|
677
679
|
|
|
678
680
|
/**
|
|
679
681
|
* Setup session replay with rrweb
|
|
682
|
+
* Captures everything for maximum replay fidelity
|
|
680
683
|
*/
|
|
681
684
|
private setupSessionReplay(): void {
|
|
682
685
|
if (!this.sessionId) {
|
|
@@ -684,22 +687,67 @@ export class CuoralIntelligence {
|
|
|
684
687
|
return;
|
|
685
688
|
}
|
|
686
689
|
|
|
687
|
-
// Start rrweb recording
|
|
690
|
+
// Start rrweb recording with aggressive capture settings
|
|
688
691
|
this.rrwebStopFn = rrweb.record({
|
|
689
692
|
emit: (event) => {
|
|
690
693
|
this.rrwebEvents.push(event);
|
|
694
|
+
this.rrwebEmit = this.rrwebEmit || ((e: any) => this.rrwebEvents.push(e)); // Store emit reference
|
|
691
695
|
},
|
|
692
|
-
|
|
696
|
+
|
|
697
|
+
// Take full snapshots more frequently to catch dynamic content
|
|
698
|
+
checkoutEveryNms: 30000, // Every 30 seconds (was 60s)
|
|
699
|
+
|
|
700
|
+
// Capture everything - minimize sampling throttling
|
|
693
701
|
sampling: {
|
|
694
|
-
scroll:
|
|
695
|
-
media: 800
|
|
696
|
-
input: 'last', // Only record final input value (privacy)
|
|
702
|
+
scroll: 50, // Capture scroll very frequently (was 150)
|
|
703
|
+
media: 200, // Capture media interactions frequently (was 800)
|
|
704
|
+
input: 'last', // Only record final input value (privacy for passwords)
|
|
705
|
+
mousemove: true, // Capture all mouse movements
|
|
706
|
+
mouseInteraction: true, // Capture all mouse interactions
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
// Privacy: Only mask sensitive inputs (passwords, credit cards)
|
|
710
|
+
// Everything else is captured for maximum replay fidelity
|
|
711
|
+
maskAllInputs: false, // Don't mask all inputs
|
|
712
|
+
maskInputOptions: {
|
|
713
|
+
password: true, // Mask password fields
|
|
714
|
+
email: false, // Capture emails
|
|
715
|
+
text: false, // Capture text inputs
|
|
716
|
+
textarea: false, // Capture textareas
|
|
717
|
+
select: false, // Capture select dropdowns
|
|
718
|
+
// Mask credit card patterns
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
// Capture all content types
|
|
722
|
+
recordCanvas: true, // Capture canvas elements (charts, games, etc.)
|
|
723
|
+
|
|
724
|
+
// Inline everything for perfect replay
|
|
725
|
+
inlineStylesheet: true, // Inline all external stylesheets
|
|
726
|
+
inlineImages: true, // Inline images as base64 (larger but more reliable)
|
|
727
|
+
collectFonts: true, // Capture custom fonts
|
|
728
|
+
|
|
729
|
+
// Block/ignore classes - customers can add these to sensitive elements
|
|
730
|
+
blockClass: 'cuoral-block', // Add to elements that should be blocked (replaced with placeholder)
|
|
731
|
+
ignoreClass: 'cuoral-ignore', // Add to elements that should be ignored (not captured)
|
|
732
|
+
|
|
733
|
+
// Capture mutations aggressively
|
|
734
|
+
slimDOMOptions: {
|
|
735
|
+
// Don't slim anything - capture full fidelity
|
|
736
|
+
script: false, // Keep scripts
|
|
737
|
+
comment: false, // Keep comments
|
|
738
|
+
headFavicon: false, // Keep favicons
|
|
739
|
+
headWhitespace: false, // Keep whitespace
|
|
740
|
+
headMetaSocial: false, // Keep meta tags
|
|
741
|
+
headMetaRobots: false, // Keep robots meta
|
|
742
|
+
headMetaHttpEquiv: false, // Keep http-equiv
|
|
743
|
+
headMetaAuthorship: false, // Keep authorship
|
|
744
|
+
headMetaDescKeywords: false, // Keep description/keywords
|
|
697
745
|
},
|
|
698
|
-
maskAllInputs: true, // Mask sensitive inputs (privacy)
|
|
699
|
-
blockClass: 'cuoral-block',
|
|
700
|
-
ignoreClass: 'cuoral-ignore',
|
|
701
746
|
});
|
|
702
747
|
|
|
748
|
+
// Setup Shadow DOM observer to capture Ionic components
|
|
749
|
+
this.setupShadowDOMObserver();
|
|
750
|
+
|
|
703
751
|
// Setup custom event tracking
|
|
704
752
|
this.setupClickTracking();
|
|
705
753
|
this.setupScrollTracking();
|
|
@@ -873,6 +921,181 @@ export class CuoralIntelligence {
|
|
|
873
921
|
});
|
|
874
922
|
}
|
|
875
923
|
|
|
924
|
+
/**
|
|
925
|
+
* Setup Shadow DOM observer to capture Ionic component content
|
|
926
|
+
* Emits custom rrweb events with Shadow DOM snapshots for viewer reconstruction
|
|
927
|
+
*/
|
|
928
|
+
private setupShadowDOMObserver(): void {
|
|
929
|
+
let shadowNodeIdCounter = 1;
|
|
930
|
+
let captureTimeout: any = null;
|
|
931
|
+
|
|
932
|
+
const captureShadowDOM = (immediate = false) => {
|
|
933
|
+
// Debounce rapid calls (e.g., during scroll)
|
|
934
|
+
if (!immediate && captureTimeout) {
|
|
935
|
+
clearTimeout(captureTimeout);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const doCapture = () => {
|
|
939
|
+
if (!this.rrwebEmit) return;
|
|
940
|
+
|
|
941
|
+
const shadowDOMData: any[] = [];
|
|
942
|
+
const elementsWithShadowDOM = document.querySelectorAll('*');
|
|
943
|
+
|
|
944
|
+
elementsWithShadowDOM.forEach((element) => {
|
|
945
|
+
if (element.shadowRoot) {
|
|
946
|
+
try {
|
|
947
|
+
// Assign unique ID to element if it doesn't have one
|
|
948
|
+
if (!this.shadowDOMNodeMap.has(element)) {
|
|
949
|
+
this.shadowDOMNodeMap.set(element, shadowNodeIdCounter++);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const nodeId = this.shadowDOMNodeMap.get(element);
|
|
953
|
+
const tagName = element.tagName.toLowerCase();
|
|
954
|
+
|
|
955
|
+
// Capture Shadow DOM HTML
|
|
956
|
+
const shadowHTML = element.shadowRoot.innerHTML;
|
|
957
|
+
|
|
958
|
+
// Capture ALL styles from Shadow DOM
|
|
959
|
+
const styleSheets: string[] = [];
|
|
960
|
+
|
|
961
|
+
// 1. Capture inline <style> tags
|
|
962
|
+
const shadowStyles = element.shadowRoot.querySelectorAll('style');
|
|
963
|
+
shadowStyles.forEach((styleEl) => {
|
|
964
|
+
if (styleEl.textContent) {
|
|
965
|
+
styleSheets.push(styleEl.textContent);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// 2. Capture adoptedStyleSheets (modern CSS API)
|
|
970
|
+
if ((element.shadowRoot as any).adoptedStyleSheets) {
|
|
971
|
+
try {
|
|
972
|
+
(element.shadowRoot as any).adoptedStyleSheets.forEach((sheet: CSSStyleSheet) => {
|
|
973
|
+
try {
|
|
974
|
+
const rules = Array.from(sheet.cssRules).map(rule => rule.cssText).join('\n');
|
|
975
|
+
if (rules) styleSheets.push(rules);
|
|
976
|
+
} catch (e) {
|
|
977
|
+
// CORS blocked stylesheet
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
} catch (e) {
|
|
981
|
+
// Browser doesn't support adoptedStyleSheets
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// 3. Capture external <link> stylesheets in Shadow DOM
|
|
986
|
+
const shadowLinks = element.shadowRoot.querySelectorAll('link[rel="stylesheet"]');
|
|
987
|
+
shadowLinks.forEach((linkEl) => {
|
|
988
|
+
const href = (linkEl as HTMLLinkElement).href;
|
|
989
|
+
if (href) {
|
|
990
|
+
// Store link href so dashboard can also load it
|
|
991
|
+
styleSheets.push(`/* @import url("${href}"); */`);
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// 4. Capture CSS custom properties from host element
|
|
996
|
+
// Ionic components use CSS variables extensively (--ion-color-*, etc.)
|
|
997
|
+
const computedStyle = window.getComputedStyle(element);
|
|
998
|
+
const cssVariables: string[] = [];
|
|
999
|
+
|
|
1000
|
+
// Get all CSS custom properties from host
|
|
1001
|
+
for (let i = 0; i < computedStyle.length; i++) {
|
|
1002
|
+
const propertyName = computedStyle[i];
|
|
1003
|
+
if (propertyName.startsWith('--')) {
|
|
1004
|
+
const propertyValue = computedStyle.getPropertyValue(propertyName).trim();
|
|
1005
|
+
if (propertyValue) {
|
|
1006
|
+
cssVariables.push(`${propertyName}: ${propertyValue};`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Add CSS variables as a :host rule
|
|
1012
|
+
if (cssVariables.length > 0) {
|
|
1013
|
+
styleSheets.push(`:host { ${cssVariables.join(' ')} }`);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Get element's CSS selector for reconstruction
|
|
1017
|
+
const selector = this.getElementSelector(element as HTMLElement);
|
|
1018
|
+
|
|
1019
|
+
// Capture element attributes (Ionic uses color="primary", mode="ios", etc.)
|
|
1020
|
+
const attributes: Record<string, string> = {};
|
|
1021
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
1022
|
+
const attr = element.attributes[i];
|
|
1023
|
+
// Skip internal attributes
|
|
1024
|
+
if (!attr.name.startsWith('data-cuoral') && !attr.name.startsWith('ng-')) {
|
|
1025
|
+
attributes[attr.name] = attr.value;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Create Shadow DOM snapshot
|
|
1030
|
+
shadowDOMData.push({
|
|
1031
|
+
nodeId,
|
|
1032
|
+
tagName,
|
|
1033
|
+
selector,
|
|
1034
|
+
attributes, // NEW: Include element attributes
|
|
1035
|
+
shadowHTML,
|
|
1036
|
+
styleSheets,
|
|
1037
|
+
timestamp: Date.now(),
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// Also set data attribute as fallback
|
|
1041
|
+
element.setAttribute('data-cuoral-shadow-id', String(nodeId));
|
|
1042
|
+
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
// Shadow DOM might be closed
|
|
1045
|
+
console.warn('[Cuoral Intelligence] Failed to capture Shadow DOM:', error);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// Emit custom event with Shadow DOM data
|
|
1051
|
+
if (shadowDOMData.length > 0 && this.rrwebEmit) {
|
|
1052
|
+
this.rrwebEmit({
|
|
1053
|
+
type: 5, // CustomEvent type in rrweb
|
|
1054
|
+
data: {
|
|
1055
|
+
tag: 'cuoral-shadow-dom',
|
|
1056
|
+
payload: {
|
|
1057
|
+
shadows: shadowDOMData,
|
|
1058
|
+
url: window.location.href,
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
timestamp: Date.now(),
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
console.log(`[Cuoral Intelligence] Captured ${shadowDOMData.length} Shadow DOM elements`);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
if (immediate) {
|
|
1069
|
+
doCapture();
|
|
1070
|
+
} else {
|
|
1071
|
+
// Debounce: wait 500ms after last call
|
|
1072
|
+
captureTimeout = setTimeout(doCapture, 500);
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// Run immediately on init
|
|
1077
|
+
captureShadowDOM(true);
|
|
1078
|
+
|
|
1079
|
+
// Observe DOM mutations to catch dynamically added Shadow DOM elements
|
|
1080
|
+
const observer = new MutationObserver(() => {
|
|
1081
|
+
captureShadowDOM(false); // Debounced
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
observer.observe(document.body, {
|
|
1085
|
+
childList: true,
|
|
1086
|
+
subtree: true,
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// Capture on scroll events (debounced)
|
|
1090
|
+
window.addEventListener('scroll', () => captureShadowDOM(false), { passive: true });
|
|
1091
|
+
|
|
1092
|
+
// Also listen for Ionic scroll events
|
|
1093
|
+
document.addEventListener('ionScroll', () => captureShadowDOM(false), { passive: true });
|
|
1094
|
+
|
|
1095
|
+
// Periodic capture as fallback (every 5 seconds now, since we have scroll capture)
|
|
1096
|
+
setInterval(() => captureShadowDOM(true), 5000);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
876
1099
|
/**
|
|
877
1100
|
* Track custom business events (flows, features, etc.)
|
|
878
1101
|
*/
|
package/src/modal.ts
CHANGED
|
@@ -9,11 +9,13 @@ export class CuoralModal {
|
|
|
9
9
|
private isOpen = false;
|
|
10
10
|
private widgetUrl: string;
|
|
11
11
|
private showFloatingButton: boolean;
|
|
12
|
+
private primaryColor: string;
|
|
12
13
|
private lastLoadedUrl?: string;
|
|
13
14
|
|
|
14
|
-
constructor(widgetUrl: string, showFloatingButton = true) {
|
|
15
|
+
constructor(widgetUrl: string, showFloatingButton = true, primaryColor = '#007AFF') {
|
|
15
16
|
this.widgetUrl = widgetUrl;
|
|
16
17
|
this.showFloatingButton = showFloatingButton;
|
|
18
|
+
this.primaryColor = primaryColor;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -94,12 +96,12 @@ export class CuoralModal {
|
|
|
94
96
|
width: '56px',
|
|
95
97
|
height: '56px',
|
|
96
98
|
borderRadius: '50%',
|
|
97
|
-
backgroundColor:
|
|
99
|
+
backgroundColor: this.primaryColor,
|
|
98
100
|
display: 'flex',
|
|
99
101
|
alignItems: 'center',
|
|
100
102
|
justifyContent: 'center',
|
|
101
103
|
cursor: 'pointer',
|
|
102
|
-
boxShadow:
|
|
104
|
+
boxShadow: `0 4px 12px ${this.primaryColor}66`, // 40% opacity
|
|
103
105
|
zIndex: '999999',
|
|
104
106
|
transition: 'transform 0.2s, box-shadow 0.2s'
|
|
105
107
|
});
|
|
@@ -108,14 +110,14 @@ export class CuoralModal {
|
|
|
108
110
|
this.floatingButton.addEventListener('mouseenter', () => {
|
|
109
111
|
if (this.floatingButton) {
|
|
110
112
|
this.floatingButton.style.transform = 'scale(1.1)';
|
|
111
|
-
this.floatingButton.style.boxShadow =
|
|
113
|
+
this.floatingButton.style.boxShadow = `0 6px 16px ${this.primaryColor}80`; // 50% opacity
|
|
112
114
|
}
|
|
113
115
|
});
|
|
114
116
|
|
|
115
117
|
this.floatingButton.addEventListener('mouseleave', () => {
|
|
116
118
|
if (this.floatingButton) {
|
|
117
119
|
this.floatingButton.style.transform = 'scale(1)';
|
|
118
|
-
this.floatingButton.style.boxShadow =
|
|
120
|
+
this.floatingButton.style.boxShadow = `0 4px 12px ${this.primaryColor}66`; // 40% opacity
|
|
119
121
|
}
|
|
120
122
|
});
|
|
121
123
|
|