@rashidazarang/airtable-mcp 1.5.0 → 2.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.
Files changed (119) hide show
  1. package/.github/ISSUE_TEMPLATE/bug-report.yml +173 -0
  2. package/.github/ISSUE_TEMPLATE/feature-request.yml +209 -0
  3. package/.github/ISSUE_TEMPLATE/security-report.yml +216 -0
  4. package/.github/pull_request_template.md +245 -0
  5. package/.github/workflows/ci-cd.yml +408 -0
  6. package/.github/workflows/security-audit.yml +316 -0
  7. package/API_DOCUMENTATION.md +897 -0
  8. package/CODE_OF_CONDUCT.md +181 -0
  9. package/Dockerfile.production +127 -0
  10. package/README.md +55 -10
  11. package/RELEASE_NOTES_v1.6.0.md +248 -0
  12. package/airtable-clipper/CHANGELOG.md +198 -0
  13. package/airtable-clipper/CHROME_STORE_SUBMISSION.md +343 -0
  14. package/airtable-clipper/LAUNCH_STRATEGY.md +495 -0
  15. package/airtable-clipper/LICENSE +21 -0
  16. package/airtable-clipper/OAUTH_SETUP.md +51 -0
  17. package/airtable-clipper/PRIVACY_POLICY.md +187 -0
  18. package/airtable-clipper/README.md +575 -0
  19. package/airtable-clipper/SUBMIT_TO_CHROME_STORE.md +273 -0
  20. package/airtable-clipper/build.sh +85 -0
  21. package/airtable-clipper/docs/QUICK_START.md +99 -0
  22. package/airtable-clipper/docs/SETUP.md +291 -0
  23. package/airtable-clipper/extension/background.js +337 -0
  24. package/airtable-clipper/extension/base-setup.html +324 -0
  25. package/airtable-clipper/extension/base-setup.js +471 -0
  26. package/airtable-clipper/extension/content.js +771 -0
  27. package/airtable-clipper/extension/icons/README.md +69 -0
  28. package/airtable-clipper/extension/icons/icon-16.png +3 -0
  29. package/airtable-clipper/extension/manifest.json +73 -0
  30. package/airtable-clipper/extension/popup.html +144 -0
  31. package/airtable-clipper/extension/popup.js +475 -0
  32. package/airtable-clipper/extension/styles/content.css +229 -0
  33. package/airtable-clipper/extension/styles/popup.css +477 -0
  34. package/airtable-clipper/privacy-policy.md +63 -0
  35. package/airtable-clipper/releases/v1.0.0/background.js +337 -0
  36. package/airtable-clipper/releases/v1.0.0/base-setup.html +324 -0
  37. package/airtable-clipper/releases/v1.0.0/base-setup.js +471 -0
  38. package/airtable-clipper/releases/v1.0.0/content.js +771 -0
  39. package/airtable-clipper/releases/v1.0.0/icons/README.md +69 -0
  40. package/airtable-clipper/releases/v1.0.0/icons/icon-128.png +2 -0
  41. package/airtable-clipper/releases/v1.0.0/icons/icon-16.png +3 -0
  42. package/airtable-clipper/releases/v1.0.0/icons/icon-32.png +2 -0
  43. package/airtable-clipper/releases/v1.0.0/icons/icon-48.png +2 -0
  44. package/airtable-clipper/releases/v1.0.0/manifest.json +73 -0
  45. package/airtable-clipper/releases/v1.0.0/popup.html +144 -0
  46. package/airtable-clipper/releases/v1.0.0/popup.js +475 -0
  47. package/airtable-clipper/releases/v1.0.0/sidepanel.html +25 -0
  48. package/airtable-clipper/releases/v1.0.0/styles/content.css +229 -0
  49. package/airtable-clipper/releases/v1.0.0/styles/popup.css +477 -0
  50. package/airtable-clipper/releases/v1.0.1/background.js +337 -0
  51. package/airtable-clipper/releases/v1.0.1/base-setup.html +324 -0
  52. package/airtable-clipper/releases/v1.0.1/base-setup.js +471 -0
  53. package/airtable-clipper/releases/v1.0.1/content.js +771 -0
  54. package/airtable-clipper/releases/v1.0.1/icons/README.md +69 -0
  55. package/airtable-clipper/releases/v1.0.1/icons/icon-128.png +2 -0
  56. package/airtable-clipper/releases/v1.0.1/icons/icon-16.png +3 -0
  57. package/airtable-clipper/releases/v1.0.1/icons/icon-32.png +2 -0
  58. package/airtable-clipper/releases/v1.0.1/icons/icon-48.png +2 -0
  59. package/airtable-clipper/releases/v1.0.1/manifest.json +70 -0
  60. package/airtable-clipper/releases/v1.0.1/popup.html +157 -0
  61. package/airtable-clipper/releases/v1.0.1/popup.js +562 -0
  62. package/airtable-clipper/releases/v1.0.1/sidepanel.html +25 -0
  63. package/airtable-clipper/releases/v1.0.1/styles/content.css +229 -0
  64. package/airtable-clipper/releases/v1.0.1/styles/popup.css +647 -0
  65. package/airtable-clipper/releases/v1.0.2/background.js +337 -0
  66. package/airtable-clipper/releases/v1.0.2/base-setup.html +324 -0
  67. package/airtable-clipper/releases/v1.0.2/base-setup.js +471 -0
  68. package/airtable-clipper/releases/v1.0.2/content.js +771 -0
  69. package/airtable-clipper/releases/v1.0.2/icons/README.md +69 -0
  70. package/airtable-clipper/releases/v1.0.2/icons/icon-128.png +2 -0
  71. package/airtable-clipper/releases/v1.0.2/icons/icon-16.png +3 -0
  72. package/airtable-clipper/releases/v1.0.2/icons/icon-32.png +2 -0
  73. package/airtable-clipper/releases/v1.0.2/icons/icon-48.png +2 -0
  74. package/airtable-clipper/releases/v1.0.2/manifest.json +62 -0
  75. package/airtable-clipper/releases/v1.0.2/popup.html +157 -0
  76. package/airtable-clipper/releases/v1.0.2/popup.js +567 -0
  77. package/airtable-clipper/releases/v1.0.2/sidepanel.html +25 -0
  78. package/airtable-clipper/releases/v1.0.2/styles/content.css +229 -0
  79. package/airtable-clipper/releases/v1.0.2/styles/popup.css +647 -0
  80. package/airtable-clipper/terms-of-service.md +124 -0
  81. package/airtable-clipper/test-credentials.md +61 -0
  82. package/airtable-clipper/test-extension/background.js +337 -0
  83. package/airtable-clipper/test-extension/base-setup.html +324 -0
  84. package/airtable-clipper/test-extension/base-setup.js +471 -0
  85. package/airtable-clipper/test-extension/content.js +873 -0
  86. package/airtable-clipper/test-extension/icons/README.md +69 -0
  87. package/airtable-clipper/test-extension/icons/icon-128.png +2 -0
  88. package/airtable-clipper/test-extension/icons/icon-16.png +3 -0
  89. package/airtable-clipper/test-extension/icons/icon-32.png +2 -0
  90. package/airtable-clipper/test-extension/icons/icon-48.png +2 -0
  91. package/airtable-clipper/test-extension/manifest.json +72 -0
  92. package/airtable-clipper/test-extension/popup.html +274 -0
  93. package/airtable-clipper/test-extension/popup.js +729 -0
  94. package/airtable-clipper/test-extension/sidepanel.html +25 -0
  95. package/airtable-clipper/test-extension/styles/content.css +229 -0
  96. package/airtable-clipper/test-extension/styles/popup.css +794 -0
  97. package/airtable_mcp_v2.js +1505 -0
  98. package/airtable_mcp_v2_oauth.js +1048 -0
  99. package/airtable_mcp_v3_advanced.js +1161 -0
  100. package/airtable_simple.js +447 -1
  101. package/airtable_simple_production.js +532 -0
  102. package/docker-compose.production.yml +366 -0
  103. package/helm/airtable-mcp/Chart.yaml +122 -0
  104. package/helm/airtable-mcp/values.yaml +538 -0
  105. package/k8s/deployment.yaml +402 -0
  106. package/k8s/namespace.yaml +108 -0
  107. package/k8s/service.yaml +194 -0
  108. package/monitoring/alerts.yml +289 -0
  109. package/monitoring/prometheus.yml +224 -0
  110. package/package.json +6 -6
  111. package/test_v1.6.0_comprehensive.sh +187 -0
  112. package/.claude/settings.local.json +0 -12
  113. package/airtable-mcp-1.1.0.tgz +0 -0
  114. package/airtable_enhanced.js +0 -499
  115. package/airtable_simple_v1.2.4_backup.js +0 -277
  116. package/airtable_v1.4.0.js +0 -654
  117. package/rashidazarang-airtable-mcp-1.1.0.tgz +0 -0
  118. package/rashidazarang-airtable-mcp-1.2.0.tgz +0 -0
  119. package/rashidazarang-airtable-mcp-1.2.1.tgz +0 -0
@@ -0,0 +1,475 @@
1
+ // Airtable Clipper Popup Script
2
+ import { AirtableClient, AirtableError } from './lib/airtable-client.js';
3
+
4
+ class PopupController {
5
+ constructor() {
6
+ this.client = null;
7
+ this.currentTab = null;
8
+ this.settings = {};
9
+ this.init();
10
+ }
11
+
12
+ async init() {
13
+ // Get current tab
14
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
15
+ this.currentTab = tabs[0];
16
+
17
+ // Load settings and initialize UI
18
+ await this.loadSettings();
19
+ this.initializeUI();
20
+ this.bindEvents();
21
+
22
+ // Check connection status
23
+ await this.checkConnection();
24
+
25
+ // Update UI based on current page
26
+ this.updateUIForCurrentPage();
27
+ }
28
+
29
+ async loadSettings() {
30
+ try {
31
+ const response = await chrome.runtime.sendMessage({
32
+ action: 'getSettings'
33
+ });
34
+
35
+ if (response.success) {
36
+ this.settings = response.settings;
37
+ this.populateSettingsUI();
38
+
39
+ if (this.settings.airtableToken && this.settings.baseId) {
40
+ this.client = new AirtableClient(this.settings.airtableToken, this.settings.baseId);
41
+ }
42
+ }
43
+ } catch (error) {
44
+ console.error('Failed to load settings:', error);
45
+ }
46
+ }
47
+
48
+ populateSettingsUI() {
49
+ // Populate configuration form
50
+ const tokenInput = document.getElementById('airtableToken');
51
+ const baseIdInput = document.getElementById('baseId');
52
+
53
+ if (tokenInput) tokenInput.value = this.settings.airtableToken || '';
54
+ if (baseIdInput) baseIdInput.value = this.settings.baseId || '';
55
+
56
+ // Populate settings
57
+ const defaultTableSelect = document.getElementById('defaultTable');
58
+ const linkedinTableSelect = document.getElementById('linkedinTable');
59
+ const autoSaveCheckbox = document.getElementById('autoSave');
60
+ const notificationsCheckbox = document.getElementById('notifications');
61
+
62
+ if (defaultTableSelect) defaultTableSelect.value = this.settings.defaultTable || 'Clips';
63
+ if (linkedinTableSelect) linkedinTableSelect.value = this.settings.linkedinTable || 'Contacts';
64
+ if (autoSaveCheckbox) autoSaveCheckbox.checked = this.settings.autoSave || false;
65
+ if (notificationsCheckbox) notificationsCheckbox.checked = this.settings.notifications !== false;
66
+ }
67
+
68
+ initializeUI() {
69
+ const isConnected = this.client !== null;
70
+
71
+ // Show/hide sections based on connection status
72
+ document.getElementById('configuration').style.display = isConnected ? 'none' : 'block';
73
+ document.getElementById('quickActions').style.display = isConnected ? 'block' : 'none';
74
+ document.getElementById('settings').style.display = 'none'; // Hidden by default
75
+
76
+ // Update connection status
77
+ this.updateConnectionStatus(isConnected);
78
+
79
+ // Load usage stats if connected
80
+ if (isConnected) {
81
+ this.loadUsageStats();
82
+ }
83
+ }
84
+
85
+ bindEvents() {
86
+ // Configuration events
87
+ document.getElementById('connectBtn')?.addEventListener('click', () => this.handleConnect());
88
+
89
+ // Quick action events
90
+ document.getElementById('quickSaveBtn')?.addEventListener('click', () => this.handleQuickSave());
91
+ document.getElementById('linkedinSaveBtn')?.addEventListener('click', () => this.handleLinkedInSave());
92
+ document.getElementById('bulkModeBtn')?.addEventListener('click', () => this.handleBulkMode());
93
+
94
+ // Footer events
95
+ document.getElementById('settingsToggle')?.addEventListener('click', () => this.toggleSettings());
96
+ document.getElementById('helpBtn')?.addEventListener('click', () => this.openHelp());
97
+ document.getElementById('sidePanelBtn')?.addEventListener('click', () => this.openSidePanel());
98
+
99
+ // Setup database button
100
+ document.getElementById('setupDatabaseBtn')?.addEventListener('click', () => this.openDatabaseSetup());
101
+
102
+ // Settings events
103
+ document.getElementById('autoSave')?.addEventListener('change', () => this.saveSettings());
104
+ document.getElementById('notifications')?.addEventListener('change', () => this.saveSettings());
105
+ document.getElementById('defaultTable')?.addEventListener('change', () => this.saveSettings());
106
+ document.getElementById('linkedinTable')?.addEventListener('change', () => this.saveSettings());
107
+
108
+ // Status message close
109
+ document.getElementById('statusClose')?.addEventListener('click', () => this.hideStatusMessage());
110
+ }
111
+
112
+ async handleConnect() {
113
+ const connectBtn = document.getElementById('connectBtn');
114
+ const connectText = document.getElementById('connectText');
115
+ const connectLoading = document.getElementById('connectLoading');
116
+
117
+ const token = document.getElementById('airtableToken').value.trim();
118
+ const baseId = document.getElementById('baseId').value.trim();
119
+
120
+ if (!token || !baseId) {
121
+ this.showStatusMessage('Please enter both token and base ID', 'error');
122
+ return;
123
+ }
124
+
125
+ // Show loading state
126
+ connectBtn.disabled = true;
127
+ connectText.textContent = 'Connecting...';
128
+ connectLoading.style.display = 'inline';
129
+
130
+ try {
131
+ // Test connection
132
+ const testClient = new AirtableClient(token, baseId);
133
+ const result = await testClient.testConnection();
134
+
135
+ if (result.success) {
136
+ // Save settings
137
+ await this.saveSettings({ airtableToken: token, baseId: baseId });
138
+
139
+ // Update client
140
+ this.client = testClient;
141
+
142
+ // Load table options
143
+ await this.loadTableOptions();
144
+
145
+ // Update UI
146
+ this.initializeUI();
147
+ this.showStatusMessage('Connected successfully!', 'success');
148
+ } else {
149
+ throw new Error(result.message);
150
+ }
151
+ } catch (error) {
152
+ console.error('Connection failed:', error);
153
+ this.showStatusMessage(`Connection failed: ${error.message}`, 'error');
154
+ } finally {
155
+ // Reset button state
156
+ connectBtn.disabled = false;
157
+ connectText.textContent = 'Connect to Airtable';
158
+ connectLoading.style.display = 'none';
159
+ }
160
+ }
161
+
162
+ async handleQuickSave() {
163
+ if (!this.client) return;
164
+
165
+ this.showLoading('Saving to Airtable...');
166
+
167
+ try {
168
+ // Send message to content script to extract page data
169
+ const response = await chrome.tabs.sendMessage(this.currentTab.id, {
170
+ action: 'extractPageData',
171
+ type: 'general'
172
+ });
173
+
174
+ if (response && response.success) {
175
+ // Save to Airtable via background script
176
+ const saveResponse = await chrome.runtime.sendMessage({
177
+ action: 'saveToAirtable',
178
+ data: {
179
+ type: 'general',
180
+ title: response.data.title,
181
+ url: response.data.url,
182
+ content: response.data.content,
183
+ timestamp: new Date().toISOString()
184
+ }
185
+ });
186
+
187
+ if (saveResponse.success) {
188
+ this.showStatusMessage('Saved to Airtable!', 'success');
189
+ this.updateUsageStats();
190
+ } else {
191
+ throw new Error(saveResponse.error);
192
+ }
193
+ } else {
194
+ // Fallback: save basic page info
195
+ await this.saveBasicPageInfo();
196
+ }
197
+ } catch (error) {
198
+ console.error('Quick save failed:', error);
199
+ this.showStatusMessage(`Save failed: ${error.message}`, 'error');
200
+ } finally {
201
+ this.hideLoading();
202
+ }
203
+ }
204
+
205
+ async handleLinkedInSave() {
206
+ if (!this.client) return;
207
+
208
+ this.showLoading('Extracting LinkedIn profile...');
209
+
210
+ try {
211
+ const response = await chrome.tabs.sendMessage(this.currentTab.id, {
212
+ action: 'extractLinkedInProfile'
213
+ });
214
+
215
+ if (response && response.success) {
216
+ const saveResponse = await chrome.runtime.sendMessage({
217
+ action: 'saveToAirtable',
218
+ data: {
219
+ type: 'linkedin',
220
+ ...response.data,
221
+ timestamp: new Date().toISOString()
222
+ }
223
+ });
224
+
225
+ if (saveResponse.success) {
226
+ this.showStatusMessage('LinkedIn profile saved!', 'success');
227
+ this.updateUsageStats();
228
+ } else {
229
+ throw new Error(saveResponse.error);
230
+ }
231
+ } else {
232
+ throw new Error('Failed to extract LinkedIn profile');
233
+ }
234
+ } catch (error) {
235
+ console.error('LinkedIn save failed:', error);
236
+ this.showStatusMessage(`LinkedIn save failed: ${error.message}`, 'error');
237
+ } finally {
238
+ this.hideLoading();
239
+ }
240
+ }
241
+
242
+ async handleBulkMode() {
243
+ try {
244
+ // Send message to content script to enter bulk mode
245
+ await chrome.tabs.sendMessage(this.currentTab.id, {
246
+ action: 'enterBulkMode'
247
+ });
248
+
249
+ // Close popup
250
+ window.close();
251
+ } catch (error) {
252
+ console.error('Bulk mode failed:', error);
253
+ this.showStatusMessage('Bulk mode not available on this page', 'error');
254
+ }
255
+ }
256
+
257
+ async saveBasicPageInfo() {
258
+ const saveResponse = await chrome.runtime.sendMessage({
259
+ action: 'saveToAirtable',
260
+ data: {
261
+ type: 'general',
262
+ title: this.currentTab.title,
263
+ url: this.currentTab.url,
264
+ timestamp: new Date().toISOString()
265
+ }
266
+ });
267
+
268
+ if (saveResponse.success) {
269
+ this.showStatusMessage('Page saved to Airtable!', 'success');
270
+ this.updateUsageStats();
271
+ } else {
272
+ throw new Error(saveResponse.error);
273
+ }
274
+ }
275
+
276
+ async loadTableOptions() {
277
+ if (!this.client) return;
278
+
279
+ try {
280
+ const tables = await this.client.listTables();
281
+
282
+ const defaultTableSelect = document.getElementById('defaultTable');
283
+ const linkedinTableSelect = document.getElementById('linkedinTable');
284
+
285
+ // Clear existing options
286
+ defaultTableSelect.innerHTML = '';
287
+ linkedinTableSelect.innerHTML = '';
288
+
289
+ // Add table options
290
+ tables.forEach(table => {
291
+ const option1 = new Option(table.name, table.name);
292
+ const option2 = new Option(table.name, table.name);
293
+
294
+ defaultTableSelect.add(option1);
295
+ linkedinTableSelect.add(option2);
296
+ });
297
+
298
+ // Set current values
299
+ defaultTableSelect.value = this.settings.defaultTable || (tables.length > 0 ? tables[0].name : '');
300
+ linkedinTableSelect.value = this.settings.linkedinTable || 'Contacts';
301
+
302
+ } catch (error) {
303
+ console.error('Failed to load tables:', error);
304
+ }
305
+ }
306
+
307
+ async checkConnection() {
308
+ if (!this.client) return false;
309
+
310
+ try {
311
+ const result = await this.client.testConnection();
312
+ this.updateConnectionStatus(result.success);
313
+ return result.success;
314
+ } catch (error) {
315
+ this.updateConnectionStatus(false);
316
+ return false;
317
+ }
318
+ }
319
+
320
+ updateConnectionStatus(isConnected) {
321
+ const statusDot = document.querySelector('.status-dot');
322
+ const statusText = document.querySelector('.status-text');
323
+
324
+ if (isConnected) {
325
+ statusDot.className = 'status-dot connected';
326
+ statusText.textContent = 'Connected';
327
+ } else {
328
+ statusDot.className = 'status-dot disconnected';
329
+ statusText.textContent = 'Not Connected';
330
+ }
331
+ }
332
+
333
+ updateUIForCurrentPage() {
334
+ if (!this.currentTab) return;
335
+
336
+ const url = this.currentTab.url;
337
+ const quickSaveBtn = document.getElementById('quickSaveBtn');
338
+ const quickSaveSubtext = document.getElementById('quickSaveSubtext');
339
+ const linkedinSaveBtn = document.getElementById('linkedinSaveBtn');
340
+
341
+ // Update quick save button text based on current page
342
+ if (url.includes('linkedin.com/in/')) {
343
+ quickSaveSubtext.textContent = 'Save LinkedIn profile';
344
+ linkedinSaveBtn.style.display = 'block';
345
+ } else if (url.includes('github.com')) {
346
+ quickSaveSubtext.textContent = 'Save GitHub repository';
347
+ } else if (url.includes('twitter.com') || url.includes('x.com')) {
348
+ quickSaveSubtext.textContent = 'Save tweet';
349
+ } else {
350
+ quickSaveSubtext.textContent = 'Save this page';
351
+ }
352
+
353
+ // Hide LinkedIn button if not on LinkedIn
354
+ if (!url.includes('linkedin.com')) {
355
+ linkedinSaveBtn.style.display = 'none';
356
+ }
357
+ }
358
+
359
+ async saveSettings(newSettings = null) {
360
+ try {
361
+ const settingsToSave = newSettings || {
362
+ defaultTable: document.getElementById('defaultTable')?.value || this.settings.defaultTable,
363
+ linkedinTable: document.getElementById('linkedinTable')?.value || this.settings.linkedinTable,
364
+ autoSave: document.getElementById('autoSave')?.checked || this.settings.autoSave,
365
+ notifications: document.getElementById('notifications')?.checked !== false,
366
+ ...this.settings
367
+ };
368
+
369
+ if (newSettings) {
370
+ Object.assign(settingsToSave, newSettings);
371
+ }
372
+
373
+ const response = await chrome.runtime.sendMessage({
374
+ action: 'saveSettings',
375
+ settings: settingsToSave
376
+ });
377
+
378
+ if (response.success) {
379
+ this.settings = settingsToSave;
380
+ }
381
+ } catch (error) {
382
+ console.error('Failed to save settings:', error);
383
+ }
384
+ }
385
+
386
+ async loadUsageStats() {
387
+ try {
388
+ const result = await chrome.storage.local.get(['usageStats']);
389
+ const stats = result.usageStats || { weeklyClips: 0, totalClips: 0 };
390
+
391
+ document.getElementById('weeklyClips').textContent = stats.weeklyClips;
392
+ document.getElementById('usageInfo').style.display = 'block';
393
+ } catch (error) {
394
+ console.error('Failed to load usage stats:', error);
395
+ }
396
+ }
397
+
398
+ async updateUsageStats() {
399
+ // Reload usage stats after saving
400
+ setTimeout(() => this.loadUsageStats(), 500);
401
+ }
402
+
403
+ toggleSettings() {
404
+ const settings = document.getElementById('settings');
405
+ const isVisible = settings.style.display !== 'none';
406
+
407
+ settings.style.display = isVisible ? 'none' : 'block';
408
+
409
+ const toggleBtn = document.getElementById('settingsToggle');
410
+ toggleBtn.textContent = isVisible ? 'Settings' : 'Hide Settings';
411
+ }
412
+
413
+ openHelp() {
414
+ chrome.tabs.create({
415
+ url: 'https://github.com/rashidazarang/airtable-clipper#readme'
416
+ });
417
+ }
418
+
419
+ async openSidePanel() {
420
+ try {
421
+ await chrome.runtime.sendMessage({
422
+ action: 'openSidePanel'
423
+ });
424
+ window.close();
425
+ } catch (error) {
426
+ console.error('Failed to open side panel:', error);
427
+ }
428
+ }
429
+
430
+ openDatabaseSetup() {
431
+ // Open the database setup wizard in a new window
432
+ chrome.tabs.create({
433
+ url: chrome.runtime.getURL('base-setup.html'),
434
+ active: true
435
+ });
436
+ window.close();
437
+ }
438
+
439
+ showStatusMessage(message, type = 'success') {
440
+ const statusMessage = document.getElementById('statusMessage');
441
+ const statusIcon = document.getElementById('statusIcon');
442
+ const statusText = document.getElementById('statusText');
443
+
444
+ statusIcon.textContent = type === 'success' ? '✅' : '❌';
445
+ statusText.textContent = message;
446
+ statusMessage.className = `status-message ${type}`;
447
+ statusMessage.style.display = 'block';
448
+
449
+ // Auto-hide after 3 seconds
450
+ setTimeout(() => this.hideStatusMessage(), 3000);
451
+ }
452
+
453
+ hideStatusMessage() {
454
+ const statusMessage = document.getElementById('statusMessage');
455
+ statusMessage.style.display = 'none';
456
+ }
457
+
458
+ showLoading(text = 'Processing...') {
459
+ const loadingOverlay = document.getElementById('loadingOverlay');
460
+ const loadingText = document.getElementById('loadingText');
461
+
462
+ loadingText.textContent = text;
463
+ loadingOverlay.style.display = 'flex';
464
+ }
465
+
466
+ hideLoading() {
467
+ const loadingOverlay = document.getElementById('loadingOverlay');
468
+ loadingOverlay.style.display = 'none';
469
+ }
470
+ }
471
+
472
+ // Initialize popup when DOM is loaded
473
+ document.addEventListener('DOMContentLoaded', () => {
474
+ new PopupController();
475
+ });
@@ -0,0 +1,229 @@
1
+ /* Airtable Clipper Content Script Styles */
2
+
3
+ /* Floating Action Button */
4
+ #airtable-clipper-fab {
5
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
6
+ }
7
+
8
+ #airtable-clipper-fab .fab-main {
9
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
10
+ }
11
+
12
+ #airtable-clipper-fab .fab-main:hover {
13
+ transform: scale(1.1) rotate(5deg);
14
+ }
15
+
16
+ #airtable-clipper-fab .fab-main:active {
17
+ transform: scale(0.95);
18
+ }
19
+
20
+ /* Selection Toolbar */
21
+ #airtable-clipper-selection-toolbar {
22
+ animation: fadeInUp 0.2s ease;
23
+ }
24
+
25
+ #airtable-clipper-selection-toolbar .selection-btn:hover {
26
+ background: rgba(255, 255, 255, 0.1);
27
+ }
28
+
29
+ /* Bulk Mode Overlay */
30
+ #airtable-clipper-bulk-overlay {
31
+ animation: slideDown 0.3s ease;
32
+ }
33
+
34
+ #airtable-clipper-bulk-overlay .bulk-btn:hover {
35
+ background: rgba(255, 255, 255, 0.3);
36
+ transform: translateY(-1px);
37
+ }
38
+
39
+ /* Bulk Selection Highlights */
40
+ .airtable-clipper-selected {
41
+ background: rgba(102, 126, 234, 0.1) !important;
42
+ border: 2px solid #667eea !important;
43
+ border-radius: 4px;
44
+ transition: all 0.2s ease;
45
+ }
46
+
47
+ .airtable-clipper-selectable:hover {
48
+ background: rgba(102, 126, 234, 0.05) !important;
49
+ cursor: pointer;
50
+ }
51
+
52
+ /* Notifications */
53
+ #airtable-clipper-notification {
54
+ animation: slideInRight 0.3s ease;
55
+ backdrop-filter: blur(10px);
56
+ }
57
+
58
+ /* Animations */
59
+ @keyframes fadeInUp {
60
+ from {
61
+ opacity: 0;
62
+ transform: translateY(10px);
63
+ }
64
+ to {
65
+ opacity: 1;
66
+ transform: translateY(0);
67
+ }
68
+ }
69
+
70
+ @keyframes slideDown {
71
+ from {
72
+ transform: translateY(-100%);
73
+ }
74
+ to {
75
+ transform: translateY(0);
76
+ }
77
+ }
78
+
79
+ @keyframes slideInRight {
80
+ from {
81
+ transform: translateX(100%);
82
+ opacity: 0;
83
+ }
84
+ to {
85
+ transform: translateX(0);
86
+ opacity: 1;
87
+ }
88
+ }
89
+
90
+ @keyframes pulse {
91
+ 0%, 100% {
92
+ opacity: 1;
93
+ }
94
+ 50% {
95
+ opacity: 0.7;
96
+ }
97
+ }
98
+
99
+ /* Loading States */
100
+ .airtable-clipper-loading {
101
+ position: relative;
102
+ opacity: 0.7;
103
+ }
104
+
105
+ .airtable-clipper-loading::after {
106
+ content: '';
107
+ position: absolute;
108
+ top: 50%;
109
+ left: 50%;
110
+ width: 20px;
111
+ height: 20px;
112
+ margin: -10px 0 0 -10px;
113
+ border: 2px solid #667eea;
114
+ border-top: 2px solid transparent;
115
+ border-radius: 50%;
116
+ animation: spin 1s linear infinite;
117
+ }
118
+
119
+ @keyframes spin {
120
+ 0% { transform: rotate(0deg); }
121
+ 100% { transform: rotate(360deg); }
122
+ }
123
+
124
+ /* Site-specific enhancements */
125
+
126
+ /* LinkedIn */
127
+ body[class*="linkedin"] .airtable-clipper-selected,
128
+ [data-test-id*="search-result"] .airtable-clipper-selected {
129
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
130
+ }
131
+
132
+ /* GitHub */
133
+ .repository-content .airtable-clipper-selected,
134
+ .Box .airtable-clipper-selected {
135
+ border-radius: 6px;
136
+ }
137
+
138
+ /* Stack Overflow */
139
+ .question-summary .airtable-clipper-selected,
140
+ .answer .airtable-clipper-selected {
141
+ border-left: 4px solid #667eea;
142
+ padding-left: 8px;
143
+ }
144
+
145
+ /* Medium */
146
+ article .airtable-clipper-selected {
147
+ border-radius: 8px;
148
+ padding: 8px;
149
+ margin: 4px 0;
150
+ }
151
+
152
+ /* Responsive Design */
153
+ @media (max-width: 768px) {
154
+ #airtable-clipper-fab {
155
+ bottom: 80px; /* Above mobile browser UI */
156
+ right: 16px;
157
+ z-index: 10000;
158
+ }
159
+
160
+ #airtable-clipper-fab .fab-main {
161
+ width: 48px;
162
+ height: 48px;
163
+ font-size: 18px;
164
+ }
165
+
166
+ #airtable-clipper-bulk-overlay {
167
+ font-size: 12px;
168
+ padding: 8px 12px;
169
+ }
170
+
171
+ #airtable-clipper-bulk-overlay .bulk-header {
172
+ flex-direction: column;
173
+ gap: 8px;
174
+ align-items: flex-start;
175
+ }
176
+
177
+ #airtable-clipper-bulk-overlay .bulk-actions {
178
+ display: flex;
179
+ gap: 8px;
180
+ }
181
+
182
+ #airtable-clipper-notification {
183
+ top: 10px;
184
+ right: 10px;
185
+ left: 10px;
186
+ max-width: none;
187
+ font-size: 13px;
188
+ }
189
+ }
190
+
191
+ /* Dark Mode Support */
192
+ @media (prefers-color-scheme: dark) {
193
+ .airtable-clipper-selected {
194
+ background: rgba(102, 126, 234, 0.2) !important;
195
+ border-color: #818cf8 !important;
196
+ }
197
+
198
+ .airtable-clipper-selectable:hover {
199
+ background: rgba(102, 126, 234, 0.1) !important;
200
+ }
201
+ }
202
+
203
+ /* High Contrast Mode */
204
+ @media (prefers-contrast: high) {
205
+ #airtable-clipper-fab .fab-main {
206
+ border: 2px solid #000;
207
+ }
208
+
209
+ .airtable-clipper-selected {
210
+ border-width: 3px !important;
211
+ background: rgba(102, 126, 234, 0.3) !important;
212
+ }
213
+ }
214
+
215
+ /* Reduced Motion */
216
+ @media (prefers-reduced-motion: reduce) {
217
+ #airtable-clipper-fab .fab-main,
218
+ #airtable-clipper-notification,
219
+ #airtable-clipper-selection-toolbar,
220
+ #airtable-clipper-bulk-overlay,
221
+ .airtable-clipper-selected {
222
+ animation: none !important;
223
+ transition: none !important;
224
+ }
225
+
226
+ #airtable-clipper-fab .fab-main:hover {
227
+ transform: none !important;
228
+ }
229
+ }