cuoral-ionic 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cuoral
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # Cuoral Ionic Library
2
+
3
+ Proactive customer success platform integration for Ionic/Capacitor applications. Cuoral provides support ticketing, customer intelligence, screen recording, and comprehensive customer engagement tools.
4
+
5
+ ## Features
6
+
7
+ ✅ **Customer Support** - Integrated support ticketing system
8
+ ✅ **Customer Intelligence** - Track and analyze customer behavior
9
+ ✅ **Screen Recording** - Native iOS screen recording for issue reproduction
10
+ ✅ **Modal Display** - Full-screen modal with floating chat button
11
+ ✅ **TypeScript Support** - Full type definitions included
12
+ ✅ **Simple API** - Just pass your public key and optional user info
13
+ ✅ **Zero Configuration** - Everything handled automatically
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install cuoral-ionic
19
+ ```
20
+
21
+ After installing, sync your Capacitor project:
22
+
23
+ ```bash
24
+ npx cap sync
25
+ ```
26
+
27
+ ### iOS Setup
28
+
29
+ Add to your `Info.plist`:
30
+
31
+ ```xml
32
+ <key>NSMicrophoneUsageDescription</key>
33
+ <string>We need access to your microphone to record audio with screen recordings</string>
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Option 1: Modal with Floating Button (Recommended)
39
+
40
+ The easiest way to integrate Cuoral - displays a floating chat button that opens the widget in a full-screen modal.
41
+
42
+ ```typescript
43
+ import { Component, OnInit, OnDestroy } from '@angular/core';
44
+ import { Cuoral } from 'cuoral-ionic';
45
+
46
+ @Component({
47
+ selector: 'app-home',
48
+ template: `
49
+ <ion-content>
50
+ <!-- Your app content -->
51
+ <h1>Welcome to Support</h1>
52
+
53
+ <!-- Optional: Custom button to open widget -->
54
+ <ion-button (click)="openSupport()"> Contact Support </ion-button>
55
+ </ion-content>
56
+ `,
57
+ })
58
+ export class HomePage implements OnInit, OnDestroy {
59
+ private cuoral: Cuoral;
60
+
61
+ constructor() {
62
+ this.cuoral = new Cuoral({
63
+ publicKey: 'your-public-key-here',
64
+ email: 'user@example.com', // Optional
65
+ firstName: 'John', // Optional
66
+ lastName: 'Doe', // Optional
67
+ showFloatingButton: true, // Show floating chat button
68
+ useModal: true, // Use modal display mode
69
+ });
70
+ }
71
+
72
+ ngOnInit() {
73
+ this.cuoral.initialize();
74
+ }
75
+
76
+ ngOnDestroy() {
77
+ this.cuoral.destroy();
78
+ }
79
+
80
+ // Open modal programmatically
81
+ openSupport() {
82
+ this.cuoral.openModal();
83
+ }
84
+ }
85
+ ```
86
+
87
+ **Features:**
88
+
89
+ - 🎈 Floating blue chat button in bottom-right corner
90
+ - 📱 Opens widget in full-screen modal with small margins
91
+ - ❌ Close button in top-right
92
+ - 🎨 Smooth animations and transitions
93
+ - 🖱️ Click backdrop to close
94
+
95
+ ### Option 2: Embedded Iframe
96
+
97
+ For more control over widget placement:
98
+
99
+ ```typescript
100
+ import { Component, OnInit, OnDestroy } from '@angular/core';
101
+ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
102
+ import { Cuoral } from 'cuoral-ionic';
103
+
104
+ @Component({
105
+ selector: 'app-support',
106
+ template: `
107
+ <ion-content>
108
+ <iframe [src]="widgetUrl" style="width: 100%; height: 600px; border: none;"> </iframe>
109
+ </ion-content>
110
+ `,
111
+ })
112
+ export class SupportPage implements OnInit, OnDestroy {
113
+ widgetUrl: SafeResourceUrl;
114
+ private cuoral: Cuoral;
115
+
116
+ constructor(private sanitizer: DomSanitizer) {
117
+ this.cuoral = new Cuoral({
118
+ publicKey: 'your-public-key-here',
119
+ useModal: false, // Disable modal mode
120
+ });
121
+
122
+ const url = this.cuoral.getWidgetUrl();
123
+ this.widgetUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
124
+ }
125
+
126
+ ngOnInit() {
127
+ this.cuoral.initialize();
128
+ }
129
+
130
+ ngOnDestroy() {
131
+ this.cuoral.destroy();
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Configuration
137
+
138
+ ### CuoralOptions
139
+
140
+ ```typescript
141
+ interface CuoralOptions {
142
+ publicKey: string; // Required: Your Cuoral public key
143
+ email?: string; // Optional: User email
144
+ firstName?: string; // Optional: User first name
145
+ lastName?: string; // Optional: User last name
146
+ debug?: boolean; // Optional: Enable debug logging (default: false)
147
+ widgetBaseUrl?: string; // Optional: Custom widget URL (default: CDN)
148
+ showFloatingButton?: boolean; // Optional: Show floating chat button (default: true)
149
+ useModal?: boolean; // Optional: Use modal display mode (default: true)
150
+ }
151
+ ```
152
+
153
+ ### Widget URL
154
+
155
+ By default, the widget loads from `https://js.cuoral.com/mobile.html` (production CDN).
156
+
157
+ **Production (Default):**
158
+
159
+ ```typescript
160
+ new Cuoral({ publicKey: 'your-key' });
161
+ ```
162
+
163
+ **Custom URL (Optional):**
164
+
165
+ If you need to use a custom widget URL for testing or self-hosting:
166
+
167
+ ```typescript
168
+ new Cuoral({
169
+ publicKey: 'your-key',
170
+ widgetBaseUrl: 'https://your-domain.com/mobile.html',
171
+ });
172
+ ```
173
+
174
+ ## API Reference
175
+
176
+ ### Cuoral Methods
177
+
178
+ ```typescript
179
+ // Initialize Cuoral
180
+ cuoral.initialize(): void
181
+
182
+ // Get widget URL for iframe embedding
183
+ cuoral.getWidgetUrl(): string
184
+
185
+ // Open modal programmatically
186
+ cuoral.openModal(): void
187
+
188
+ // Close modal programmatically
189
+ cuoral.closeModal(): void
190
+
191
+ // Check if modal is open
192
+ cuoral.isModalOpen(): boolean
193
+
194
+ // Clean up resources
195
+ cuoral.destroy(): void
196
+ ```
197
+
198
+ ## What Gets Handled Automatically
199
+
200
+ - ✅ Support ticket creation and management
201
+ - ✅ Customer intelligence tracking
202
+ - ✅ Screen recording start/stop
203
+ - ✅ File path conversion for video playback
204
+ - ✅ Communication between widget and native code
205
+ - ✅ Video upload to Cuoral backend
206
+ - ✅ State management
207
+ - ✅ Permissions handling
208
+
209
+ ## Troubleshooting
210
+
211
+ ### Recording doesn't start
212
+
213
+ **Check iOS permissions:**
214
+
215
+ - Verify `NSMicrophoneUsageDescription` is in Info.plist
216
+ - System will prompt user automatically on first recording
217
+
218
+ **Enable debug mode:**
219
+
220
+ ```typescript
221
+ new Cuoral({
222
+ publicKey: 'your-key',
223
+ debug: true, // See console logs
224
+ });
225
+ ```
226
+
227
+ ### Widget not appearing
228
+
229
+ **Modal mode:**
230
+
231
+ - Make sure you called `cuoral.initialize()`
232
+ - Check that `useModal: true` is set (default)
233
+ - Look for the blue floating button in bottom-right
234
+
235
+ **Iframe mode:**
236
+
237
+ - Verify `widgetUrl` is properly sanitized with `DomSanitizer`
238
+ - Check browser console for CORS errors
239
+ - Ensure iframe has proper dimensions in CSS
240
+
241
+ ### Build errors
242
+
243
+ **iOS Pod Issues:**
244
+
245
+ ```bash
246
+ cd ios
247
+ pod deintegrate
248
+ pod install
249
+ cd ..
250
+ npx cap sync ios
251
+ ```
252
+
253
+ **Clean build:**
254
+
255
+ ```bash
256
+ rm -rf node_modules dist
257
+ npm install
258
+ npx cap sync
259
+ ```
260
+
261
+ ## Support
262
+
263
+ - 📧 Email: support@cuoral.com
264
+ - 📖 Docs: https://docs.cuoral.com
265
+ - 🐛 Issues: https://github.com/cuoral/cuoral-ionic/issues
266
+
267
+ ## License
268
+
269
+ MIT License - see [LICENSE](LICENSE) file
270
+
271
+ ---
272
+
273
+ Built with ❤️ by Cuoral
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
+ package="com.cuoral.ionic">
4
+
5
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
6
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
7
+ android:maxSdkVersion="28" />
8
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
9
+
10
+ </manifest>
@@ -0,0 +1,291 @@
1
+ package com.cuoral.ionic;
2
+
3
+ import android.Manifest;
4
+ import android.app.Activity;
5
+ import android.content.Context;
6
+ import android.content.Intent;
7
+ import android.content.pm.PackageManager;
8
+ import android.graphics.Bitmap;
9
+ import android.media.MediaRecorder;
10
+ import android.media.projection.MediaProjection;
11
+ import android.media.projection.MediaProjectionManager;
12
+ import android.os.Build;
13
+ import android.util.Base64;
14
+ import android.util.DisplayMetrics;
15
+ import android.view.View;
16
+
17
+ import androidx.core.app.ActivityCompat;
18
+ import androidx.core.content.ContextCompat;
19
+
20
+ import com.getcapacitor.JSObject;
21
+ import com.getcapacitor.Plugin;
22
+ import com.getcapacitor.PluginCall;
23
+ import com.getcapacitor.PluginMethod;
24
+ import com.getcapacitor.annotation.CapacitorPlugin;
25
+ import com.getcapacitor.annotation.Permission;
26
+ import com.getcapacitor.annotation.ActivityCallback;
27
+
28
+ import java.io.ByteArrayOutputStream;
29
+ import java.io.File;
30
+ import java.io.IOException;
31
+
32
+ @CapacitorPlugin(name = "CuoralPlugin", permissions = {
33
+ @Permission(strings = { Manifest.permission.RECORD_AUDIO }, alias = "audio"),
34
+ @Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "storage")
35
+ })
36
+ public class CuoralPlugin extends Plugin {
37
+
38
+ private static final int SCREEN_CAPTURE_REQUEST_CODE = 1001;
39
+ private static final int PERMISSION_REQUEST_CODE = 1002;
40
+
41
+ private MediaProjectionManager projectionManager;
42
+ private MediaProjection mediaProjection;
43
+ private MediaRecorder mediaRecorder;
44
+ private boolean isRecording = false;
45
+ private long recordingStartTime = 0;
46
+ private String videoFilePath;
47
+ private PluginCall currentCall;
48
+
49
+ @Override
50
+ public void load() {
51
+ super.load();
52
+ projectionManager = (MediaProjectionManager) getContext()
53
+ .getSystemService(Context.MEDIA_PROJECTION_SERVICE);
54
+ }
55
+
56
+ @PluginMethod
57
+ public void startRecording(PluginCall call) {
58
+ if (isRecording) {
59
+ call.resolve(createErrorResult("Already recording"));
60
+ return;
61
+ }
62
+
63
+ // Save call for later use
64
+ currentCall = call;
65
+
66
+ // Check if we need audio permission
67
+ boolean includeAudio = call.getBoolean("includeAudio", false);
68
+ if (includeAudio && !hasAudioPermission()) {
69
+ requestPermissionForAlias("audio", call, "audioPermissionCallback");
70
+ return;
71
+ }
72
+
73
+ // Request screen capture
74
+ Intent captureIntent = projectionManager.createScreenCaptureIntent();
75
+ startActivityForResult(call, captureIntent, "screenCaptureCallback");
76
+ }
77
+
78
+ @ActivityCallback
79
+ private void screenCaptureCallback(PluginCall call, com.getcapacitor.PluginResult result) {
80
+ if (result.getResultCode() != Activity.RESULT_OK) {
81
+ call.reject("Screen capture permission denied");
82
+ return;
83
+ }
84
+
85
+ Intent data = result.getData();
86
+ mediaProjection = projectionManager.getMediaProjection(result.getResultCode(), data);
87
+
88
+ // Setup media recorder
89
+ boolean includeAudio = call.getBoolean("includeAudio", false);
90
+ float quality = call.getFloat("quality", 1.0f);
91
+
92
+ try {
93
+ setupMediaRecorder(includeAudio, quality);
94
+ mediaRecorder.start();
95
+ isRecording = true;
96
+ recordingStartTime = System.currentTimeMillis();
97
+
98
+ JSObject ret = new JSObject();
99
+ ret.put("success", true);
100
+ call.resolve(ret);
101
+
102
+ // Notify listeners
103
+ JSObject eventData = new JSObject();
104
+ eventData.put("timestamp", recordingStartTime);
105
+ notifyListeners("recordingStarted", eventData);
106
+
107
+ } catch (Exception e) {
108
+ call.reject("Failed to start recording: " + e.getMessage());
109
+ }
110
+ }
111
+
112
+ @ActivityCallback
113
+ private void audioPermissionCallback(PluginCall call) {
114
+ if (!hasAudioPermission()) {
115
+ call.reject("Audio permission required");
116
+ return;
117
+ }
118
+ // Retry starting recording
119
+ startRecording(call);
120
+ }
121
+
122
+ @PluginMethod
123
+ public void stopRecording(PluginCall call) {
124
+ if (!isRecording) {
125
+ call.resolve(createErrorResult("Not recording"));
126
+ return;
127
+ }
128
+
129
+ try {
130
+ mediaRecorder.stop();
131
+ mediaRecorder.reset();
132
+ mediaRecorder.release();
133
+ mediaRecorder = null;
134
+
135
+ if (mediaProjection != null) {
136
+ mediaProjection.stop();
137
+ mediaProjection = null;
138
+ }
139
+
140
+ isRecording = false;
141
+ long duration = (System.currentTimeMillis() - recordingStartTime) / 1000;
142
+
143
+ JSObject ret = new JSObject();
144
+ ret.put("success", true);
145
+ ret.put("filePath", videoFilePath);
146
+ ret.put("duration", duration);
147
+ call.resolve(ret);
148
+
149
+ // Notify listeners
150
+ JSObject eventData = new JSObject();
151
+ eventData.put("filePath", videoFilePath);
152
+ eventData.put("duration", duration);
153
+ notifyListeners("recordingStopped", eventData);
154
+
155
+ } catch (Exception e) {
156
+ call.reject("Failed to stop recording: " + e.getMessage());
157
+ }
158
+ }
159
+
160
+ @PluginMethod
161
+ public void getRecordingState(PluginCall call) {
162
+ long duration = isRecording ? (System.currentTimeMillis() - recordingStartTime) / 1000 : 0;
163
+
164
+ JSObject ret = new JSObject();
165
+ ret.put("isRecording", isRecording);
166
+ ret.put("duration", duration);
167
+ ret.put("filePath", videoFilePath != null ? videoFilePath : "");
168
+ call.resolve(ret);
169
+ }
170
+
171
+ @PluginMethod
172
+ public void takeScreenshot(PluginCall call) {
173
+ try {
174
+ Activity activity = getActivity();
175
+ View rootView = activity.getWindow().getDecorView().getRootView();
176
+ rootView.setDrawingCacheEnabled(true);
177
+
178
+ Bitmap bitmap = Bitmap.createBitmap(rootView.getDrawingCache());
179
+ rootView.setDrawingCacheEnabled(false);
180
+
181
+ // Get options
182
+ float quality = call.getFloat("quality", 0.92f);
183
+ String format = call.getString("format", "png");
184
+
185
+ // Convert to base64
186
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
187
+ if (format.equals("jpeg")) {
188
+ bitmap.compress(Bitmap.CompressFormat.JPEG, (int) (quality * 100), outputStream);
189
+ } else {
190
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
191
+ }
192
+
193
+ byte[] bytes = outputStream.toByteArray();
194
+ String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
195
+
196
+ JSObject ret = new JSObject();
197
+ ret.put("base64", base64);
198
+ ret.put("mimeType", "image/" + format);
199
+ ret.put("width", bitmap.getWidth());
200
+ ret.put("height", bitmap.getHeight());
201
+ call.resolve(ret);
202
+
203
+ // Notify listeners
204
+ JSObject eventData = new JSObject();
205
+ eventData.put("success", true);
206
+ notifyListeners("screenshotTaken", eventData);
207
+
208
+ bitmap.recycle();
209
+
210
+ } catch (Exception e) {
211
+ call.reject("Failed to take screenshot: " + e.getMessage());
212
+ }
213
+ }
214
+
215
+ @PluginMethod
216
+ public void isRecordingSupported(PluginCall call) {
217
+ JSObject ret = new JSObject();
218
+ ret.put("supported", Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
219
+ call.resolve(ret);
220
+ }
221
+
222
+ @PluginMethod
223
+ public void requestPermissions(PluginCall call) {
224
+ if (hasRequiredPermissions()) {
225
+ JSObject ret = new JSObject();
226
+ ret.put("granted", true);
227
+ call.resolve(ret);
228
+ } else {
229
+ requestAllPermissions(call, "permissionsCallback");
230
+ }
231
+ }
232
+
233
+ @ActivityCallback
234
+ private void permissionsCallback(PluginCall call) {
235
+ JSObject ret = new JSObject();
236
+ ret.put("granted", hasRequiredPermissions());
237
+ call.resolve(ret);
238
+ }
239
+
240
+ // Helper methods
241
+
242
+ private void setupMediaRecorder(boolean includeAudio, float quality) throws IOException {
243
+ DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
244
+
245
+ File outputDir = getContext().getFilesDir();
246
+ videoFilePath = new File(outputDir, "cuoral_recording_" + System.currentTimeMillis() + ".mp4")
247
+ .getAbsolutePath();
248
+
249
+ mediaRecorder = new MediaRecorder();
250
+
251
+ if (includeAudio) {
252
+ mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
253
+ }
254
+ mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
255
+ mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
256
+ mediaRecorder.setOutputFile(videoFilePath);
257
+
258
+ int bitRate = (int) (6000000 * quality); // Base bitrate 6Mbps
259
+ mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
260
+ mediaRecorder.setVideoSize(metrics.widthPixels, metrics.heightPixels);
261
+ mediaRecorder.setVideoFrameRate(30);
262
+ mediaRecorder.setVideoEncodingBitRate(bitRate);
263
+
264
+ if (includeAudio) {
265
+ mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
266
+ mediaRecorder.setAudioEncodingBitRate(128000);
267
+ mediaRecorder.setAudioSamplingRate(44100);
268
+ }
269
+
270
+ mediaRecorder.prepare();
271
+ }
272
+
273
+ private boolean hasAudioPermission() {
274
+ return ContextCompat.checkSelfPermission(getContext(),
275
+ Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
276
+ }
277
+
278
+ private boolean hasRequiredPermissions() {
279
+ return hasAudioPermission() &&
280
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
281
+ ContextCompat.checkSelfPermission(getContext(),
282
+ Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
283
+ }
284
+
285
+ private JSObject createErrorResult(String message) {
286
+ JSObject ret = new JSObject();
287
+ ret.put("success", false);
288
+ ret.put("error", message);
289
+ return ret;
290
+ }
291
+ }
Binary file
@@ -0,0 +1,51 @@
1
+ import { CuoralMessage, CuoralMessageType, CuoralConfig } from './types';
2
+ /**
3
+ * Bridge for bidirectional communication between WebView and Native code
4
+ */
5
+ export declare class CuoralBridge {
6
+ private config;
7
+ private messageHandlers;
8
+ private isInitialized;
9
+ private widgetIframe;
10
+ constructor(config: CuoralConfig);
11
+ /**
12
+ * Initialize the bridge
13
+ */
14
+ initialize(): void;
15
+ /**
16
+ * Send message to native code
17
+ */
18
+ sendToNative(message: CuoralMessage): void;
19
+ /**
20
+ * Send message to widget iframe
21
+ */
22
+ sendToWidget(message: CuoralMessage): void;
23
+ /**
24
+ * Register message handler
25
+ */
26
+ on(type: CuoralMessageType, handler: (payload: any) => void): () => void;
27
+ /**
28
+ * Setup window message listener
29
+ */
30
+ private setupMessageListener;
31
+ /**
32
+ * Debug logging
33
+ */
34
+ private log;
35
+ /**
36
+ * Destroy the bridge
37
+ */
38
+ destroy(): void;
39
+ }
40
+ declare global {
41
+ interface Window {
42
+ webkit?: {
43
+ messageHandlers?: {
44
+ cuoral?: {
45
+ postMessage: (message: any) => void;
46
+ };
47
+ };
48
+ };
49
+ }
50
+ }
51
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEzE;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,eAAe,CAAoE;IAC3F,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,YAAY,CAAkC;gBAE1C,MAAM,EAAE,YAAY;IAKhC;;OAEG;IACI,UAAU,IAAI,IAAI;IAgBzB;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAoBjD;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAyCjD;;OAEG;IACI,EAAE,CAAC,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI;IAmB/E;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAqC5B;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACI,OAAO,IAAI,IAAI;CAKvB;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,MAAM,CAAC,EAAE;YACP,eAAe,CAAC,EAAE;gBAChB,MAAM,CAAC,EAAE;oBACP,WAAW,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;iBACrC,CAAC;aACH,CAAC;SACH,CAAC;KACH;CACF"}