chrometools-mcp 2.4.2 → 3.1.2

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +540 -0
  2. package/COMPONENT_MAPPING_SPEC.md +1217 -0
  3. package/README.md +494 -38
  4. package/bridge/bridge-client.js +472 -0
  5. package/bridge/bridge-service.js +399 -0
  6. package/bridge/install.js +241 -0
  7. package/browser/browser-manager.js +107 -2
  8. package/browser/page-manager.js +226 -69
  9. package/docs/CHROME_EXTENSION.md +219 -0
  10. package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
  11. package/element-finder-utils.js +138 -28
  12. package/extension/background.js +643 -0
  13. package/extension/content.js +715 -0
  14. package/extension/icons/create-icons.js +164 -0
  15. package/extension/icons/icon128.png +0 -0
  16. package/extension/icons/icon16.png +0 -0
  17. package/extension/icons/icon48.png +0 -0
  18. package/extension/manifest.json +58 -0
  19. package/extension/popup/popup.css +437 -0
  20. package/extension/popup/popup.html +102 -0
  21. package/extension/popup/popup.js +415 -0
  22. package/extension/recorder-overlay.css +93 -0
  23. package/figma-tools.js +120 -0
  24. package/index.js +3347 -2518
  25. package/models/BaseInputModel.js +93 -0
  26. package/models/CheckboxGroupModel.js +199 -0
  27. package/models/CheckboxModel.js +103 -0
  28. package/models/ColorInputModel.js +53 -0
  29. package/models/DateInputModel.js +67 -0
  30. package/models/RadioGroupModel.js +126 -0
  31. package/models/RangeInputModel.js +60 -0
  32. package/models/SelectModel.js +97 -0
  33. package/models/TextInputModel.js +34 -0
  34. package/models/TextareaModel.js +59 -0
  35. package/models/TimeInputModel.js +49 -0
  36. package/models/index.js +122 -0
  37. package/package.json +3 -2
  38. package/pom/apom-converter.js +267 -0
  39. package/pom/apom-tree-converter.js +515 -0
  40. package/pom/element-id-generator.js +175 -0
  41. package/recorder/page-object-generator.js +16 -0
  42. package/recorder/scenario-executor.js +80 -2
  43. package/server/tool-definitions.js +839 -656
  44. package/server/tool-groups.js +3 -2
  45. package/server/tool-schemas.js +367 -296
  46. package/server/websocket-bridge.js +447 -0
  47. package/utils/selector-resolver.js +186 -0
  48. package/utils/ui-framework-detector.js +392 -0
@@ -0,0 +1,102 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ChromeTools MCP</title>
7
+ <link rel="stylesheet" href="popup.css">
8
+ </head>
9
+ <body>
10
+ <div class="popup-container">
11
+ <!-- Header -->
12
+ <header class="header">
13
+ <div class="logo">
14
+ <span class="logo-icon">CT</span>
15
+ <span class="logo-text">ChromeTools</span>
16
+ </div>
17
+ <div class="connection-status" id="connection-status">
18
+ <span class="status-dot"></span>
19
+ <span class="status-text">Disconnected</span>
20
+ </div>
21
+ </header>
22
+
23
+ <!-- Tabs -->
24
+ <nav class="tabs">
25
+ <button class="tab active" data-tab="recorder">Recorder</button>
26
+ <button class="tab" data-tab="tabs">Tabs</button>
27
+ </nav>
28
+
29
+ <!-- Recorder Tab -->
30
+ <section class="tab-content active" id="tab-recorder">
31
+ <!-- Recording Status -->
32
+ <div class="recording-status" id="recording-status">
33
+ <span class="recording-dot"></span>
34
+ <span class="recording-text">Not recording</span>
35
+ </div>
36
+
37
+ <!-- Metadata Form (shown when not recording) -->
38
+ <div class="metadata-form" id="metadata-form">
39
+ <div class="form-group">
40
+ <label for="scenario-name">Scenario Name *</label>
41
+ <input type="text" id="scenario-name" placeholder="e.g., login-flow" required>
42
+ </div>
43
+ <div class="form-group">
44
+ <label for="scenario-desc">Description</label>
45
+ <input type="text" id="scenario-desc" placeholder="Optional description">
46
+ </div>
47
+ <div class="form-group">
48
+ <label for="scenario-tags">Tags</label>
49
+ <input type="text" id="scenario-tags" placeholder="auth, critical, smoke">
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Action Count (shown when recording) -->
54
+ <div class="action-count-display hidden" id="action-count-display">
55
+ <span class="count-number" id="action-count">0</span>
56
+ <span class="count-label">actions recorded</span>
57
+ </div>
58
+
59
+ <!-- Controls -->
60
+ <div class="controls">
61
+ <button class="btn btn-primary" id="btn-start">
62
+ <span class="btn-icon">&#9679;</span> Start Recording
63
+ </button>
64
+ <button class="btn btn-secondary hidden" id="btn-pause">
65
+ <span class="btn-icon">&#10074;&#10074;</span> Pause
66
+ </button>
67
+ <button class="btn btn-danger hidden" id="btn-stop">
68
+ <span class="btn-icon">&#9632;</span> Stop & Save
69
+ </button>
70
+ <button class="btn btn-ghost hidden" id="btn-clear">
71
+ Clear
72
+ </button>
73
+ <button class="btn btn-reset hidden" id="btn-reset">
74
+ &#10006; Reset
75
+ </button>
76
+ </div>
77
+
78
+ <!-- Actions Preview -->
79
+ <div class="actions-preview hidden" id="actions-preview">
80
+ <h4>Recent Actions</h4>
81
+ <ul class="actions-list" id="actions-list">
82
+ <!-- Actions will be populated here -->
83
+ </ul>
84
+ </div>
85
+ </section>
86
+
87
+ <!-- Tabs Tab -->
88
+ <section class="tab-content" id="tab-tabs">
89
+ <div class="tabs-list" id="tabs-list">
90
+ <div class="tabs-loading">Loading tabs...</div>
91
+ </div>
92
+ </section>
93
+
94
+ <!-- Footer -->
95
+ <footer class="footer">
96
+ <span class="version">v1.0.0</span>
97
+ </footer>
98
+ </div>
99
+
100
+ <script src="popup.js"></script>
101
+ </body>
102
+ </html>
@@ -0,0 +1,415 @@
1
+ /**
2
+ * ChromeTools MCP Extension - Popup Script
3
+ */
4
+
5
+ // DOM Elements
6
+ const elements = {
7
+ // Status
8
+ connectionStatus: document.getElementById('connection-status'),
9
+ recordingStatus: document.getElementById('recording-status'),
10
+
11
+ // Tabs navigation
12
+ tabButtons: document.querySelectorAll('.tab'),
13
+ tabContents: document.querySelectorAll('.tab-content'),
14
+
15
+ // Recorder
16
+ metadataForm: document.getElementById('metadata-form'),
17
+ actionCountDisplay: document.getElementById('action-count-display'),
18
+ actionCount: document.getElementById('action-count'),
19
+ actionsPreview: document.getElementById('actions-preview'),
20
+ actionsList: document.getElementById('actions-list'),
21
+
22
+ // Form fields
23
+ scenarioName: document.getElementById('scenario-name'),
24
+ scenarioDesc: document.getElementById('scenario-desc'),
25
+ scenarioTags: document.getElementById('scenario-tags'),
26
+
27
+ // Buttons
28
+ btnStart: document.getElementById('btn-start'),
29
+ btnPause: document.getElementById('btn-pause'),
30
+ btnStop: document.getElementById('btn-stop'),
31
+ btnClear: document.getElementById('btn-clear'),
32
+ btnReset: document.getElementById('btn-reset'),
33
+
34
+ // Tabs list
35
+ tabsList: document.getElementById('tabs-list')
36
+ };
37
+
38
+ // State
39
+ let currentState = {
40
+ isRecording: false,
41
+ isPaused: false,
42
+ actions: [],
43
+ isConnected: false,
44
+ scenarioName: '',
45
+ scenarioDescription: '',
46
+ scenarioTags: []
47
+ };
48
+
49
+ // ============================================
50
+ // Tab Navigation
51
+ // ============================================
52
+
53
+ elements.tabButtons.forEach(button => {
54
+ button.addEventListener('click', () => {
55
+ const tabName = button.dataset.tab;
56
+
57
+ // Update buttons
58
+ elements.tabButtons.forEach(b => b.classList.remove('active'));
59
+ button.classList.add('active');
60
+
61
+ // Update content
62
+ elements.tabContents.forEach(content => {
63
+ content.classList.remove('active');
64
+ if (content.id === `tab-${tabName}`) {
65
+ content.classList.add('active');
66
+ }
67
+ });
68
+
69
+ // Load tabs list if switching to tabs tab
70
+ if (tabName === 'tabs') {
71
+ loadTabsList();
72
+ }
73
+ });
74
+ });
75
+
76
+ // ============================================
77
+ // State Management
78
+ // ============================================
79
+
80
+ async function loadState() {
81
+ try {
82
+ const response = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
83
+ currentState = {
84
+ isRecording: response.isRecording || false,
85
+ isPaused: response.isPaused || false,
86
+ actions: response.actions || [],
87
+ isConnected: response.isConnected || false,
88
+ scenarioName: response.scenarioName || '',
89
+ scenarioDescription: response.scenarioDescription || '',
90
+ scenarioTags: response.scenarioTags || []
91
+ };
92
+ updateUI();
93
+ } catch (error) {
94
+ console.error('Failed to load state:', error);
95
+ }
96
+ }
97
+
98
+ function updateUI() {
99
+ // Connection status
100
+ if (currentState.isConnected) {
101
+ elements.connectionStatus.classList.add('connected');
102
+ elements.connectionStatus.querySelector('.status-text').textContent = 'Connected';
103
+ } else {
104
+ elements.connectionStatus.classList.remove('connected');
105
+ elements.connectionStatus.querySelector('.status-text').textContent = 'Disconnected';
106
+ }
107
+
108
+ // Recording status
109
+ elements.recordingStatus.classList.remove('recording', 'paused');
110
+ if (currentState.isRecording) {
111
+ if (currentState.isPaused) {
112
+ elements.recordingStatus.classList.add('paused');
113
+ elements.recordingStatus.querySelector('.recording-text').textContent = 'Paused';
114
+ } else {
115
+ elements.recordingStatus.classList.add('recording');
116
+ elements.recordingStatus.querySelector('.recording-text').textContent = 'Recording...';
117
+ }
118
+ } else {
119
+ elements.recordingStatus.querySelector('.recording-text').textContent = 'Not recording';
120
+ }
121
+
122
+ // Form vs Action Count
123
+ if (currentState.isRecording) {
124
+ elements.metadataForm.classList.add('hidden');
125
+ elements.actionCountDisplay.classList.remove('hidden');
126
+ elements.actionCount.textContent = currentState.actions.length;
127
+
128
+ // Show actions preview
129
+ if (currentState.actions.length > 0) {
130
+ elements.actionsPreview.classList.remove('hidden');
131
+ updateActionsList();
132
+ }
133
+ } else {
134
+ elements.metadataForm.classList.remove('hidden');
135
+ elements.actionCountDisplay.classList.add('hidden');
136
+ elements.actionsPreview.classList.add('hidden');
137
+ }
138
+
139
+ // Buttons
140
+ if (currentState.isRecording) {
141
+ elements.btnStart.classList.add('hidden');
142
+ elements.btnPause.classList.remove('hidden');
143
+ elements.btnStop.classList.remove('hidden');
144
+ elements.btnClear.classList.remove('hidden');
145
+ elements.btnReset.classList.remove('hidden');
146
+
147
+ // Update pause button text
148
+ if (currentState.isPaused) {
149
+ elements.btnPause.innerHTML = '<span class="btn-icon">&#9658;</span> Resume';
150
+ } else {
151
+ elements.btnPause.innerHTML = '<span class="btn-icon">&#10074;&#10074;</span> Pause';
152
+ }
153
+ } else {
154
+ elements.btnStart.classList.remove('hidden');
155
+ elements.btnPause.classList.add('hidden');
156
+ elements.btnStop.classList.add('hidden');
157
+ elements.btnClear.classList.add('hidden');
158
+ elements.btnReset.classList.add('hidden');
159
+ }
160
+
161
+ // Disable start if not connected or no name
162
+ elements.btnStart.disabled = !currentState.isConnected;
163
+ }
164
+
165
+ function updateActionsList() {
166
+ const recentActions = currentState.actions.slice(-5).reverse();
167
+
168
+ elements.actionsList.innerHTML = recentActions.map(action => {
169
+ let details = '';
170
+ switch (action.type) {
171
+ case 'click':
172
+ details = action.data?.text || action.selector?.primary || '';
173
+ break;
174
+ case 'type':
175
+ details = action.data?.isSecret ? '***' : (action.data?.text || '').substring(0, 20);
176
+ break;
177
+ case 'select':
178
+ details = action.data?.text || action.data?.value || '';
179
+ break;
180
+ case 'keypress':
181
+ details = (action.data?.modifiers || []).concat(action.data?.key || '').join('+');
182
+ break;
183
+ default:
184
+ details = action.selector?.primary || '';
185
+ }
186
+
187
+ return `
188
+ <li>
189
+ <span class="action-type">${action.type}</span>
190
+ <span class="action-details">${escapeHtml(details)}</span>
191
+ </li>
192
+ `;
193
+ }).join('');
194
+ }
195
+
196
+ function escapeHtml(text) {
197
+ const div = document.createElement('div');
198
+ div.textContent = text;
199
+ return div.innerHTML;
200
+ }
201
+
202
+ // ============================================
203
+ // Button Handlers
204
+ // ============================================
205
+
206
+ elements.btnStart.addEventListener('click', async () => {
207
+ const name = elements.scenarioName.value.trim();
208
+ const description = elements.scenarioDesc.value.trim();
209
+ const tags = elements.scenarioTags.value.split(',').map(t => t.trim()).filter(Boolean);
210
+
211
+ if (!name) {
212
+ elements.scenarioName.focus();
213
+ elements.scenarioName.style.borderColor = '#ef4444';
214
+ setTimeout(() => {
215
+ elements.scenarioName.style.borderColor = '';
216
+ }, 2000);
217
+ return;
218
+ }
219
+
220
+ try {
221
+ await chrome.runtime.sendMessage({
222
+ type: 'START_RECORDING',
223
+ options: {
224
+ name,
225
+ description,
226
+ tags
227
+ }
228
+ });
229
+
230
+ // Save metadata in state for later use
231
+ currentState.isRecording = true;
232
+ currentState.isPaused = false;
233
+ currentState.actions = [];
234
+ currentState.scenarioName = name;
235
+ currentState.scenarioDescription = description;
236
+ currentState.scenarioTags = tags;
237
+ updateUI();
238
+ } catch (error) {
239
+ console.error('Failed to start recording:', error);
240
+ alert('Failed to start recording: ' + error.message);
241
+ }
242
+ });
243
+
244
+ elements.btnPause.addEventListener('click', async () => {
245
+ try {
246
+ const response = await chrome.runtime.sendMessage({ type: 'PAUSE_RECORDING' });
247
+ currentState.isPaused = response.isPaused;
248
+ updateUI();
249
+ } catch (error) {
250
+ console.error('Failed to pause recording:', error);
251
+ }
252
+ });
253
+
254
+ elements.btnStop.addEventListener('click', async () => {
255
+ // Use saved scenario name from state (form is hidden during recording)
256
+ const name = currentState.scenarioName || elements.scenarioName.value.trim();
257
+
258
+ if (!name) {
259
+ alert('Please enter a scenario name');
260
+ return;
261
+ }
262
+
263
+ try {
264
+ // First, stop recording to get actions and secrets
265
+ const stopResult = await chrome.runtime.sendMessage({ type: 'STOP_RECORDING' });
266
+
267
+ if (!stopResult.success) {
268
+ throw new Error('Failed to stop recording');
269
+ }
270
+
271
+ // Then save the scenario with collected data
272
+ await chrome.runtime.sendMessage({
273
+ type: 'SAVE_SCENARIO',
274
+ scenario: {
275
+ name,
276
+ description: currentState.scenarioDescription || elements.scenarioDesc.value.trim(),
277
+ tags: currentState.scenarioTags.length > 0 ? currentState.scenarioTags : elements.scenarioTags.value.split(',').map(t => t.trim()).filter(Boolean),
278
+ actions: stopResult.actions || [],
279
+ secrets: stopResult.secrets || {},
280
+ metadata: stopResult.metadata || {}
281
+ }
282
+ });
283
+
284
+ // Reset form and state
285
+ elements.scenarioName.value = '';
286
+ elements.scenarioDesc.value = '';
287
+ elements.scenarioTags.value = '';
288
+
289
+ currentState.isRecording = false;
290
+ currentState.isPaused = false;
291
+ currentState.actions = [];
292
+ currentState.scenarioName = '';
293
+ currentState.scenarioDescription = '';
294
+ currentState.scenarioTags = [];
295
+ updateUI();
296
+
297
+ // Show success message
298
+ alert('Scenario saved successfully!');
299
+
300
+ // Force reload state from background to ensure sync
301
+ await loadState();
302
+ } catch (error) {
303
+ console.error('Failed to save scenario:', error);
304
+ alert('Failed to save scenario: ' + error.message);
305
+ }
306
+ });
307
+
308
+ elements.btnClear.addEventListener('click', async () => {
309
+ if (!confirm('Clear all recorded actions?')) return;
310
+
311
+ try {
312
+ await chrome.runtime.sendMessage({ type: 'CLEAR_ACTIONS' });
313
+ currentState.actions = [];
314
+ updateUI();
315
+ } catch (error) {
316
+ console.error('Failed to clear actions:', error);
317
+ }
318
+ });
319
+
320
+ elements.btnReset.addEventListener('click', async () => {
321
+ if (!confirm('Force reset recording? All recorded actions will be lost.')) return;
322
+
323
+ try {
324
+ await chrome.runtime.sendMessage({ type: 'FORCE_RESET' });
325
+
326
+ // Reset local state
327
+ currentState.isRecording = false;
328
+ currentState.isPaused = false;
329
+ currentState.actions = [];
330
+ currentState.scenarioName = '';
331
+ currentState.scenarioDescription = '';
332
+ currentState.scenarioTags = [];
333
+
334
+ // Reset form
335
+ elements.scenarioName.value = '';
336
+ elements.scenarioDesc.value = '';
337
+ elements.scenarioTags.value = '';
338
+
339
+ updateUI();
340
+ } catch (error) {
341
+ console.error('Failed to reset:', error);
342
+ alert('Failed to reset: ' + error.message);
343
+ }
344
+ });
345
+
346
+ // ============================================
347
+ // Tabs List
348
+ // ============================================
349
+
350
+ async function loadTabsList() {
351
+ elements.tabsList.innerHTML = '<div class="tabs-loading">Loading tabs...</div>';
352
+
353
+ try {
354
+ const tabs = await chrome.tabs.query({});
355
+
356
+ if (tabs.length === 0) {
357
+ elements.tabsList.innerHTML = '<div class="tabs-loading">No tabs open</div>';
358
+ return;
359
+ }
360
+
361
+ elements.tabsList.innerHTML = tabs.map(tab => `
362
+ <div class="tab-item ${tab.active ? 'active' : ''}" data-tab-id="${tab.id}">
363
+ <img class="tab-favicon" src="${tab.favIconUrl || ''}" alt="" onerror="this.style.display='none'">
364
+ <div class="tab-info">
365
+ <div class="tab-title">${escapeHtml(tab.title || 'Untitled')}</div>
366
+ <div class="tab-url">${escapeHtml(tab.url || '')}</div>
367
+ </div>
368
+ ${tab.active ? '<span class="tab-active-badge">Active</span>' : ''}
369
+ </div>
370
+ `).join('');
371
+
372
+ // Add click handlers
373
+ elements.tabsList.querySelectorAll('.tab-item').forEach(item => {
374
+ item.addEventListener('click', () => {
375
+ const tabId = parseInt(item.dataset.tabId);
376
+ chrome.tabs.update(tabId, { active: true });
377
+ window.close();
378
+ });
379
+ });
380
+
381
+ } catch (error) {
382
+ console.error('Failed to load tabs:', error);
383
+ elements.tabsList.innerHTML = '<div class="tabs-loading">Failed to load tabs</div>';
384
+ }
385
+ }
386
+
387
+ // ============================================
388
+ // Message Listener
389
+ // ============================================
390
+
391
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
392
+ switch (message.type) {
393
+ case 'ACTION_RECORDED':
394
+ currentState.actions.push({}); // Just increment count
395
+ elements.actionCount.textContent = message.actionCount || currentState.actions.length;
396
+ break;
397
+
398
+ case 'SCENARIO_SAVED':
399
+ if (message.success) {
400
+ // Already handled in btnStop click
401
+ } else {
402
+ alert('Failed to save scenario: ' + (message.error || 'Unknown error'));
403
+ }
404
+ break;
405
+ }
406
+ });
407
+
408
+ // ============================================
409
+ // Initialization
410
+ // ============================================
411
+
412
+ loadState();
413
+
414
+ // Refresh state periodically
415
+ setInterval(loadState, 2000);
@@ -0,0 +1,93 @@
1
+ /* ChromeTools MCP - Recorder Overlay Styles */
2
+
3
+ #chrometools-recorder-overlay {
4
+ position: fixed;
5
+ top: 10px;
6
+ right: 10px;
7
+ padding: 8px 16px;
8
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
9
+ color: white;
10
+ border-radius: 20px;
11
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
12
+ font-size: 12px;
13
+ font-weight: 500;
14
+ z-index: 2147483647;
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 8px;
18
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
19
+ cursor: move;
20
+ user-select: none;
21
+ transition: opacity 0.2s, transform 0.2s;
22
+ }
23
+
24
+ #chrometools-recorder-overlay:hover {
25
+ transform: scale(1.02);
26
+ }
27
+
28
+ #chrometools-recorder-overlay.minimized {
29
+ width: 40px;
30
+ height: 40px;
31
+ padding: 0;
32
+ border-radius: 50%;
33
+ justify-content: center;
34
+ }
35
+
36
+ #chrometools-recorder-overlay.minimized .recorder-text {
37
+ display: none;
38
+ }
39
+
40
+ #chrometools-recorder-overlay .recorder-dot {
41
+ width: 8px;
42
+ height: 8px;
43
+ background: white;
44
+ border-radius: 50%;
45
+ animation: chrometools-pulse 1.5s ease-in-out infinite;
46
+ flex-shrink: 0;
47
+ }
48
+
49
+ @keyframes chrometools-pulse {
50
+ 0%, 100% {
51
+ opacity: 1;
52
+ transform: scale(1);
53
+ }
54
+ 50% {
55
+ opacity: 0.5;
56
+ transform: scale(1.1);
57
+ }
58
+ }
59
+
60
+ #chrometools-recorder-overlay .recorder-text {
61
+ white-space: nowrap;
62
+ }
63
+
64
+ #chrometools-recorder-overlay .action-count {
65
+ background: rgba(255, 255, 255, 0.2);
66
+ padding: 2px 8px;
67
+ border-radius: 10px;
68
+ font-size: 11px;
69
+ }
70
+
71
+ /* Highlight for recorded elements */
72
+ .chrometools-highlight {
73
+ outline: 2px solid #10b981 !important;
74
+ outline-offset: 2px !important;
75
+ background-color: rgba(16, 185, 129, 0.1) !important;
76
+ transition: all 0.2s ease !important;
77
+ }
78
+
79
+ /* Recording indicator on body */
80
+ body.chrometools-recording {
81
+ outline: 3px solid rgba(239, 68, 68, 0.5) !important;
82
+ outline-offset: -3px !important;
83
+ }
84
+
85
+ /* Paused state */
86
+ #chrometools-recorder-overlay.paused {
87
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
88
+ }
89
+
90
+ #chrometools-recorder-overlay.paused .recorder-dot {
91
+ animation: none;
92
+ background: rgba(255, 255, 255, 0.6);
93
+ }