App.svelte
1 <script lang="ts"> 2 import { onMount } from 'svelte'; 3 import { isFileSystemAccessSupported, getFilesystemBridge } from '../../lib/sync/filesystem-bridge'; 4 import { getLogseqClient, type LogseqSettings, DEFAULT_LOGSEQ_SETTINGS } from '../../lib/logseq'; 5 import { getFileSystemService, isFileSystemAccessSupported as isGraphAccessSupported } from '../../lib/filesystem'; 6 import { DEFAULT_PAUSE_MEDIA_SITES } from '../../lib/pause-media-defaults'; 7 import { todoistAuth, todoistClient, workspaceSyncService } from '../../lib/todoist'; 8 import type { TodoistSettings, TodoistConnectionState } from '../../lib/todoist'; 9 10 interface Settings { 11 autoSave: boolean; 12 autoSaveInterval: number; 13 showOrphanPrompt: boolean; 14 syncBackend: 'none' | 'native' | 'filesystem'; 15 confirmContextSwitch: boolean; 16 restoreWindowPositions: boolean; 17 useSmartPositioning: boolean; 18 theme: 'system' | 'light' | 'dark'; 19 lazyLoadTabs: boolean; 20 pauseMediaSites?: string[]; 21 } 22 23 let settings = $state<Settings>({ 24 autoSave: true, 25 autoSaveInterval: 30000, 26 showOrphanPrompt: true, 27 syncBackend: 'none', 28 confirmContextSwitch: true, 29 restoreWindowPositions: true, 30 useSmartPositioning: true, 31 theme: 'system', 32 lazyLoadTabs: true, 33 pauseMediaSites: [], 34 }); 35 36 // Pause media sites editing 37 let pauseMediaEditMode = $state(false); 38 let pauseMediaText = $state(''); 39 let pauseMediaSaved = $state(false); 40 41 let saving = $state(false); 42 let saved = $state(false); 43 44 // Filesystem sync 45 const filesystemSupported = isFileSystemAccessSupported(); 46 const filesystemBridge = getFilesystemBridge(); 47 let filesystemConnected = $state(false); 48 let filesystemDirectory = $state<string | null>(null); 49 let selectingDirectory = $state(false); 50 let filesystemError = $state<string | null>(null); 51 52 // Native messaging sync 53 let nativeConnected = $state(false); 54 let nativeConfigDir = $state<string | null>(null); 55 let nativeLastSync = $state<string | null>(null); 56 let nativeTesting = $state(false); 57 let nativeSyncing = $state(false); 58 let nativeError = $state<string | null>(null); 59 60 // Claude settings 61 let claudeApiKey = $state(''); 62 let claudeEnabled = $state(false); 63 let claudeAutoSuggest = $state(false); 64 let claudeTesting = $state(false); 65 let claudeTestResult = $state<'success' | 'error' | null>(null); 66 let claudeTestError = $state(''); 67 68 // Logseq settings 69 const logseqClient = getLogseqClient(); 70 let logseqSettings = $state<LogseqSettings>({ ...DEFAULT_LOGSEQ_SETTINGS }); 71 let logseqConnected = $state(false); 72 let logseqGraphName = $state<string | null>(null); 73 let logseqTesting = $state(false); 74 let logseqTestResult = $state<'success' | 'error' | null>(null); 75 76 // Logseq graph access (for file attachments) 77 const graphAccessSupported = isGraphAccessSupported(); 78 const graphAccessService = getFileSystemService(); 79 let graphAccessGranted = $state(false); 80 let graphAccessDirectory = $state<string | null>(null); 81 let graphAccessRequesting = $state(false); 82 83 // Todoist settings 84 let todoistEnabled = $state(false); 85 let todoistConnection = $state<TodoistConnectionState | null>(null); 86 let todoistApiToken = $state(''); 87 let todoistTesting = $state(false); 88 let todoistTestResult = $state<'success' | 'error' | null>(null); 89 let todoistAutoSync = $state(true); 90 91 // Credential vault settings 92 let vaultExists = $state(false); 93 let vaultUnlocked = $state(false); 94 let vaultCredentials = $state({ claude: false, todoist: false, logseq: false }); 95 let vaultSetupMode = $state(false); 96 let vaultChangingPassword = $state(false); 97 let vaultOldPassword = $state(''); 98 let vaultNewPassword = $state(''); 99 let vaultConfirmPassword = $state(''); 100 let vaultError = $state<string | null>(null); 101 let vaultSuccess = $state<string | null>(null); 102 103 onMount(async () => { 104 const result = await browser.storage.local.get('workspaceConfig'); 105 if (result.workspaceConfig?.settings) { 106 settings = { ...settings, ...result.workspaceConfig.settings }; 107 } 108 109 // Check filesystem sync status 110 if (settings.syncBackend === 'filesystem' && filesystemSupported) { 111 const connected = await filesystemBridge.connect(); 112 filesystemConnected = connected; 113 if (connected) { 114 const state = filesystemBridge.getState(); 115 filesystemDirectory = state.configDir || null; 116 } 117 } 118 119 // Check native messaging sync status 120 if (settings.syncBackend === 'native') { 121 await checkNativeConnection(); 122 } 123 124 // Load Claude settings 125 try { 126 const claudeResponse = await browser.runtime.sendMessage({ type: 'GET_CLAUDE_SETTINGS' }); 127 claudeEnabled = claudeResponse.enabled || false; 128 claudeAutoSuggest = claudeResponse.autoSuggest || false; 129 // API key is masked, so we don't set it 130 } catch { 131 // Claude not configured 132 } 133 134 // Load Logseq settings 135 try { 136 const stored = await browser.storage.local.get('logseqSettings'); 137 if (stored.logseqSettings) { 138 logseqSettings = { ...logseqSettings, ...stored.logseqSettings }; 139 // Create plain object copy to avoid Firefox proxy serialization issues 140 logseqClient.setSettings({ ...logseqSettings }); 141 } 142 143 // Check connection if enabled 144 if (logseqSettings.enabled) { 145 const state = await logseqClient.checkConnection(); 146 logseqConnected = state.available; 147 logseqGraphName = state.graphName; 148 } 149 150 // Restore graph access for attachments 151 if (graphAccessSupported) { 152 const hasAccess = await graphAccessService.restoreAccess(); 153 const graphState = graphAccessService.getState(); 154 graphAccessGranted = hasAccess; 155 graphAccessDirectory = graphState.directoryPath; 156 } 157 } catch { 158 // Logseq not configured 159 } 160 161 // Load Todoist settings 162 try { 163 const todoistSettings = await todoistAuth.loadSettings(); 164 todoistEnabled = todoistSettings.enabled || false; 165 166 if (todoistEnabled && todoistSettings.accessToken) { 167 todoistConnection = await todoistClient.initializeFromStorage(); 168 } 169 } catch { 170 // Todoist not configured 171 } 172 173 // Load vault status and credentials if unlocked 174 await loadVaultStatus(); 175 if (vaultUnlocked && (vaultCredentials.claude || vaultCredentials.todoist || vaultCredentials.logseq)) { 176 await loadCredentialsFromVault(); 177 } 178 }); 179 180 async function loadVaultStatus() { 181 try { 182 const vaultResponse = await browser.runtime.sendMessage({ type: 'VAULT_STATUS' }); 183 if (vaultResponse.success) { 184 vaultExists = vaultResponse.exists; 185 vaultUnlocked = vaultResponse.unlocked; 186 vaultCredentials = vaultResponse.credentials || { claude: false, todoist: false, logseq: false }; 187 } 188 } catch { 189 // Vault not available (native bridge not connected) 190 } 191 } 192 193 async function loadCredentialsFromVault() { 194 // Load Claude credentials from vault 195 if (vaultCredentials.claude) { 196 try { 197 const claudeResponse = await browser.runtime.sendMessage({ 198 type: 'VAULT_GET_CREDENTIAL', 199 payload: { type: 'claude' }, 200 }); 201 if (claudeResponse.success && claudeResponse.credential) { 202 const creds = claudeResponse.credential; 203 // Set the API key in the background service 204 if (creds.apiKey) { 205 await browser.runtime.sendMessage({ 206 type: 'SET_CLAUDE_API_KEY', 207 payload: { apiKey: creds.apiKey }, 208 }); 209 claudeEnabled = true; 210 claudeAutoSuggest = creds.autoSuggest || false; 211 } 212 } 213 } catch (e) { 214 console.warn('[Mnemonic] Failed to load Claude credentials from vault:', e); 215 } 216 } 217 218 // Load Todoist credentials from vault 219 // Note: Save directly to settings instead of re-validating via API call. 220 // authenticateWithApiToken() makes an external fetch to api.todoist.com which 221 // can fail transiently (network, rate-limit, DNS on fresh boot), causing the 222 // token to silently not restore. The token was already validated when first stored. 223 if (vaultCredentials.todoist) { 224 try { 225 const todoistResponse = await browser.runtime.sendMessage({ 226 type: 'VAULT_GET_CREDENTIAL', 227 payload: { type: 'todoist' }, 228 }); 229 if (todoistResponse.success && todoistResponse.credential) { 230 const creds = todoistResponse.credential; 231 if (creds.accessToken) { 232 // Restore settings directly — skip API re-validation 233 await todoistAuth.saveSettings({ 234 enabled: true, 235 authMethod: creds.authMethod || 'api_token', 236 accessToken: creds.accessToken, 237 masterProjectId: creds.masterProjectId, 238 }); 239 todoistEnabled = true; 240 todoistConnection = await todoistClient.initializeFromStorage(); 241 } 242 } 243 } catch (e) { 244 console.warn('[Mnemonic] Failed to load Todoist credentials from vault:', e); 245 } 246 } 247 248 // Load Logseq credentials from vault 249 if (vaultCredentials.logseq) { 250 try { 251 const logseqResponse = await browser.runtime.sendMessage({ 252 type: 'VAULT_GET_CREDENTIAL', 253 payload: { type: 'logseq' }, 254 }); 255 if (logseqResponse.success && logseqResponse.credential) { 256 const creds = logseqResponse.credential; 257 if (creds.authToken) { 258 logseqSettings = { 259 ...logseqSettings, 260 enabled: true, 261 apiUrl: creds.apiUrl || logseqSettings.apiUrl, 262 authToken: creds.authToken, 263 autoSync: creds.autoSync ?? logseqSettings.autoSync, 264 syncInterval: creds.syncInterval ?? logseqSettings.syncInterval, 265 }; 266 logseqClient.setSettings({ ...logseqSettings }); 267 await browser.storage.local.set({ logseqSettings: { ...logseqSettings } }); 268 269 // Check connection 270 const state = await logseqClient.checkConnection(); 271 logseqConnected = state.available; 272 logseqGraphName = state.graphName; 273 } 274 } 275 } catch (e) { 276 console.warn('[Mnemonic] Failed to load Logseq credentials from vault:', e); 277 } 278 } 279 } 280 281 async function handleSelectDirectory() { 282 selectingDirectory = true; 283 filesystemError = null; 284 try { 285 const success = await filesystemBridge.selectDirectory(); 286 if (success) { 287 filesystemConnected = true; 288 const state = filesystemBridge.getState(); 289 filesystemDirectory = state.configDir || null; 290 // Notify background script of filesystem connection 291 await browser.runtime.sendMessage({ 292 type: 'INIT_SYNC_BACKEND', 293 payload: { backendType: 'filesystem' }, 294 }); 295 } 296 } catch (e) { 297 filesystemError = e instanceof Error ? e.message : 'Failed to select directory'; 298 } finally { 299 selectingDirectory = false; 300 } 301 } 302 303 async function handleSyncBackendChange() { 304 // Re-initialize sync when backend changes 305 if (settings.syncBackend === 'filesystem' && filesystemSupported) { 306 const connected = await filesystemBridge.connect(); 307 filesystemConnected = connected; 308 if (connected) { 309 const state = filesystemBridge.getState(); 310 filesystemDirectory = state.configDir || null; 311 } 312 } else { 313 filesystemConnected = false; 314 filesystemDirectory = null; 315 } 316 317 // Check native connection when switching to native 318 if (settings.syncBackend === 'native') { 319 await checkNativeConnection(); 320 } else { 321 nativeConnected = false; 322 nativeConfigDir = null; 323 nativeError = null; 324 } 325 } 326 327 async function checkNativeConnection() { 328 nativeTesting = true; 329 nativeError = null; 330 try { 331 // First initialize the sync backend 332 await browser.runtime.sendMessage({ 333 type: 'INIT_SYNC_BACKEND', 334 payload: { backendType: 'native' }, 335 }); 336 337 // Then test the connection 338 const result = await browser.runtime.sendMessage({ type: 'TEST_SYNC_CONNECTION' }); 339 if (result.success) { 340 nativeConnected = true; 341 nativeConfigDir = result.info?.configDir || null; 342 // Get sync state for last sync time 343 const state = await browser.runtime.sendMessage({ type: 'GET_SYNC_STATE' }); 344 nativeLastSync = state.state?.lastSyncTime || null; 345 } else { 346 nativeConnected = false; 347 nativeError = result.error || 'Failed to connect to native host'; 348 } 349 } catch (e) { 350 nativeConnected = false; 351 nativeError = e instanceof Error ? e.message : 'Connection test failed'; 352 } finally { 353 nativeTesting = false; 354 } 355 } 356 357 async function handleSyncNow() { 358 nativeSyncing = true; 359 nativeError = null; 360 try { 361 const result = await browser.runtime.sendMessage({ type: 'SYNC_NOW' }); 362 if (result.success) { 363 nativeLastSync = new Date().toISOString(); 364 } else { 365 nativeError = result.error || 'Sync failed'; 366 } 367 } catch (e) { 368 nativeError = e instanceof Error ? e.message : 'Sync failed'; 369 } finally { 370 nativeSyncing = false; 371 } 372 } 373 374 function formatLastSync(isoString: string | null): string { 375 if (!isoString) return 'Never'; 376 try { 377 const date = new Date(isoString); 378 const now = new Date(); 379 const diffMs = now.getTime() - date.getTime(); 380 const diffMins = Math.floor(diffMs / 60000); 381 const diffHours = Math.floor(diffMs / 3600000); 382 383 if (diffMins < 1) return 'Just now'; 384 if (diffMins < 60) return `${diffMins} min ago`; 385 if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; 386 return date.toLocaleDateString(); 387 } catch { 388 return 'Unknown'; 389 } 390 } 391 392 async function saveSettings() { 393 saving = true; 394 try { 395 const result = await browser.storage.local.get('workspaceConfig'); 396 const config = result.workspaceConfig || {}; 397 // Create plain object copy to avoid Firefox DataCloneError with Svelte 5 proxies 398 config.settings = { ...settings }; 399 config.metadata = { 400 ...config.metadata, 401 lastModified: new Date().toISOString(), 402 }; 403 await browser.storage.local.set({ workspaceConfig: config }); 404 saved = true; 405 setTimeout(() => (saved = false), 2000); 406 } catch (e) { 407 console.error('[Mnemonic] Failed to save settings:', e); 408 } finally { 409 saving = false; 410 } 411 } 412 413 async function saveClaudeApiKey() { 414 if (!claudeApiKey.trim()) return; 415 416 try { 417 await browser.runtime.sendMessage({ 418 type: 'SET_CLAUDE_API_KEY', 419 payload: { apiKey: claudeApiKey.trim() }, 420 }); 421 claudeEnabled = true; 422 claudeApiKey = ''; // Clear from UI for security 423 } catch (e) { 424 console.error('[Mnemonic] Failed to save Claude API key:', e); 425 } 426 } 427 428 async function clearClaudeApiKey() { 429 try { 430 await browser.runtime.sendMessage({ 431 type: 'SET_CLAUDE_API_KEY', 432 payload: { apiKey: null }, 433 }); 434 claudeEnabled = false; 435 claudeApiKey = ''; 436 } catch (e) { 437 console.error('[Mnemonic] Failed to clear Claude API key:', e); 438 } 439 } 440 441 async function testClaudeConnection() { 442 claudeTesting = true; 443 claudeTestResult = null; 444 claudeTestError = ''; 445 446 try { 447 const response = await browser.runtime.sendMessage({ type: 'TEST_CLAUDE_CONNECTION' }); 448 claudeTestResult = response.success ? 'success' : 'error'; 449 claudeTestError = response.error || ''; 450 } catch { 451 claudeTestResult = 'error'; 452 claudeTestError = 'Failed to connect to background service'; 453 } finally { 454 claudeTesting = false; 455 setTimeout(() => { 456 claudeTestResult = null; 457 claudeTestError = ''; 458 }, 5000); 459 } 460 } 461 462 async function updateClaudeAutoSuggest() { 463 try { 464 await browser.runtime.sendMessage({ 465 type: 'UPDATE_CLAUDE_SETTINGS', 466 payload: { autoSuggest: claudeAutoSuggest }, 467 }); 468 } catch (e) { 469 console.error('[Mnemonic] Failed to update Claude settings:', e); 470 } 471 } 472 473 // Logseq functions 474 async function saveLogseqSettings() { 475 try { 476 // Create plain object copy to avoid Firefox DataCloneError with Svelte 5 proxies 477 const settingsToSave = { ...logseqSettings }; 478 await browser.storage.local.set({ logseqSettings: settingsToSave }); 479 logseqClient.setSettings(settingsToSave); 480 481 // Check connection if enabled 482 if (logseqSettings.enabled) { 483 const state = await logseqClient.checkConnection(); 484 logseqConnected = state.available; 485 logseqGraphName = state.graphName; 486 } else { 487 logseqConnected = false; 488 logseqGraphName = null; 489 } 490 } catch (e) { 491 console.error('[Mnemonic] Failed to save Logseq settings:', e); 492 } 493 } 494 495 async function testLogseqConnection() { 496 logseqTesting = true; 497 logseqTestResult = null; 498 499 try { 500 // Unwrap proxy before passing to client 501 logseqClient.setSettings({ ...logseqSettings }); 502 const state = await logseqClient.checkConnection(); 503 logseqConnected = state.available; 504 logseqGraphName = state.graphName; 505 logseqTestResult = state.available ? 'success' : 'error'; 506 } catch { 507 logseqTestResult = 'error'; 508 logseqConnected = false; 509 logseqGraphName = null; 510 } finally { 511 logseqTesting = false; 512 setTimeout(() => (logseqTestResult = null), 3000); 513 } 514 } 515 516 function toggleLogseqEnabled() { 517 logseqSettings.enabled = !logseqSettings.enabled; 518 saveLogseqSettings(); 519 } 520 521 // Graph access functions (for file attachments) 522 async function requestGraphAccess() { 523 if (!graphAccessSupported) return; 524 525 graphAccessRequesting = true; 526 try { 527 const granted = await graphAccessService.requestAccess(); 528 const state = graphAccessService.getState(); 529 graphAccessGranted = granted; 530 graphAccessDirectory = state.directoryPath; 531 } catch (e) { 532 console.error('[Mnemonic] Failed to request graph access:', e); 533 graphAccessGranted = false; 534 } finally { 535 graphAccessRequesting = false; 536 } 537 } 538 539 async function revokeGraphAccess() { 540 if (!graphAccessSupported) return; 541 542 try { 543 await graphAccessService.revokeAccess(); 544 graphAccessGranted = false; 545 graphAccessDirectory = null; 546 } catch (e) { 547 console.error('[Mnemonic] Failed to revoke graph access:', e); 548 } 549 } 550 551 // Todoist functions 552 async function saveTodoistApiToken() { 553 if (!todoistApiToken.trim()) return; 554 555 todoistTesting = true; 556 todoistTestResult = null; 557 558 try { 559 const result = await todoistAuth.authenticateWithApiToken(todoistApiToken.trim()); 560 561 if (result.success) { 562 todoistEnabled = true; 563 todoistConnection = await todoistClient.initializeFromStorage(); 564 todoistApiToken = ''; // Clear from UI for security 565 todoistTestResult = 'success'; 566 } else { 567 todoistTestResult = 'error'; 568 } 569 } catch { 570 todoistTestResult = 'error'; 571 } finally { 572 todoistTesting = false; 573 setTimeout(() => (todoistTestResult = null), 3000); 574 } 575 } 576 577 async function testTodoistConnection() { 578 todoistTesting = true; 579 todoistTestResult = null; 580 581 try { 582 todoistConnection = await todoistClient.initializeFromStorage(); 583 todoistTestResult = todoistConnection?.connected ? 'success' : 'error'; 584 } catch { 585 todoistTestResult = 'error'; 586 } finally { 587 todoistTesting = false; 588 setTimeout(() => (todoistTestResult = null), 3000); 589 } 590 } 591 592 async function disconnectTodoist() { 593 await todoistClient.disconnect(); 594 await workspaceSyncService.clearAllMappings(); 595 todoistEnabled = false; 596 todoistConnection = null; 597 todoistApiToken = ''; 598 } 599 600 // Credential vault functions 601 async function handleSetupVault() { 602 if (vaultNewPassword.length < 8) { 603 vaultError = 'Password must be at least 8 characters'; 604 return; 605 } 606 if (vaultNewPassword !== vaultConfirmPassword) { 607 vaultError = 'Passwords do not match'; 608 return; 609 } 610 611 vaultError = null; 612 vaultSuccess = null; 613 614 try { 615 const response = await browser.runtime.sendMessage({ 616 type: 'VAULT_SETUP', 617 payload: { password: vaultNewPassword }, 618 }); 619 620 if (response.success) { 621 vaultSetupMode = false; 622 vaultNewPassword = ''; 623 vaultConfirmPassword = ''; 624 vaultSuccess = 'Master password created successfully'; 625 await loadVaultStatus(); 626 setTimeout(() => (vaultSuccess = null), 3000); 627 } else { 628 vaultError = response.error || 'Failed to setup vault'; 629 } 630 } catch (e) { 631 vaultError = e instanceof Error ? e.message : 'Failed to setup vault'; 632 } 633 } 634 635 async function handleChangeVaultPassword() { 636 if (vaultNewPassword.length < 8) { 637 vaultError = 'New password must be at least 8 characters'; 638 return; 639 } 640 if (vaultNewPassword !== vaultConfirmPassword) { 641 vaultError = 'New passwords do not match'; 642 return; 643 } 644 645 vaultError = null; 646 vaultSuccess = null; 647 648 try { 649 const response = await browser.runtime.sendMessage({ 650 type: 'VAULT_CHANGE_PASSWORD', 651 payload: { oldPassword: vaultOldPassword, newPassword: vaultNewPassword }, 652 }); 653 654 if (response.success) { 655 vaultChangingPassword = false; 656 vaultOldPassword = ''; 657 vaultNewPassword = ''; 658 vaultConfirmPassword = ''; 659 vaultSuccess = 'Master password changed successfully'; 660 setTimeout(() => (vaultSuccess = null), 3000); 661 } else { 662 vaultError = response.error || 'Failed to change password'; 663 } 664 } catch (e) { 665 vaultError = e instanceof Error ? e.message : 'Failed to change password'; 666 } 667 } 668 669 async function handleLockVault() { 670 try { 671 await browser.runtime.sendMessage({ type: 'VAULT_LOCK' }); 672 await loadVaultStatus(); 673 } catch (e) { 674 vaultError = e instanceof Error ? e.message : 'Failed to lock vault'; 675 } 676 } 677 678 async function handleUnlockVault() { 679 if (!vaultOldPassword) return; 680 681 vaultError = null; 682 vaultSuccess = null; 683 684 try { 685 const response = await browser.runtime.sendMessage({ 686 type: 'VAULT_UNLOCK', 687 payload: { password: vaultOldPassword }, 688 }); 689 690 if (response.success) { 691 vaultOldPassword = ''; 692 vaultSuccess = 'Vault unlocked - restoring integrations...'; 693 await loadVaultStatus(); 694 // Load and restore credentials from vault 695 await loadCredentialsFromVault(); 696 vaultSuccess = 'Vault unlocked and integrations restored'; 697 setTimeout(() => (vaultSuccess = null), 3000); 698 } else { 699 vaultError = response.error || 'Incorrect password'; 700 } 701 } catch (e) { 702 vaultError = e instanceof Error ? e.message : 'Failed to unlock vault'; 703 } 704 } 705 706 // Pause media sites functions 707 function startEditPauseMediaSites() { 708 // Combine default sites and custom sites for editing 709 const customSites = settings.pauseMediaSites || []; 710 const allSites = [ 711 '# Default sites (pre-injected for better performance)', 712 ...DEFAULT_PAUSE_MEDIA_SITES, 713 '', 714 '# Your custom sites (add below this line)', 715 '# Sites with ports (e.g., :3000) work via programmatic injection', 716 ...customSites, 717 ]; 718 pauseMediaText = allSites.join('\n'); 719 pauseMediaEditMode = true; 720 } 721 722 function cancelEditPauseMediaSites() { 723 pauseMediaEditMode = false; 724 pauseMediaText = ''; 725 } 726 727 async function savePauseMediaSites() { 728 // Parse the text and extract only custom sites (not defaults) 729 const lines = pauseMediaText.split('\n'); 730 const customSites: string[] = []; 731 let inCustomSection = false; 732 733 for (const line of lines) { 734 const trimmed = line.trim(); 735 736 // Track when we enter the custom section 737 if (trimmed.includes('Your custom sites') || trimmed.includes('custom sites')) { 738 inCustomSection = true; 739 continue; 740 } 741 742 // Skip empty lines and comments 743 if (!trimmed || trimmed.startsWith('#')) continue; 744 745 // Skip default sites 746 if (DEFAULT_PAUSE_MEDIA_SITES.includes(trimmed)) continue; 747 748 // Add to custom sites if we're in custom section or it's not a default 749 customSites.push(trimmed); 750 } 751 752 // Update settings 753 settings.pauseMediaSites = customSites; 754 await saveSettings(); 755 756 pauseMediaEditMode = false; 757 pauseMediaText = ''; 758 pauseMediaSaved = true; 759 setTimeout(() => (pauseMediaSaved = false), 2000); 760 } 761 762 function resetPauseMediaToDefaults() { 763 settings.pauseMediaSites = []; 764 saveSettings(); 765 pauseMediaSaved = true; 766 setTimeout(() => (pauseMediaSaved = false), 2000); 767 } 768 </script> 769 770 <div class="options"> 771 <header class="options-header"> 772 <div class="logo-mark">M</div> 773 <h1 class="options-title">Mnemonic Settings</h1> 774 </header> 775 776 <main class="options-content"> 777 <section class="settings-section"> 778 <h2 class="section-title">Auto-Save</h2> 779 <div class="setting-item"> 780 <label class="setting-label"> 781 <input 782 type="checkbox" 783 bind:checked={settings.autoSave} 784 class="checkbox" 785 /> 786 <span>Automatically save workspace state</span> 787 </label> 788 <p class="setting-description"> 789 Periodically save open tabs and window positions 790 </p> 791 </div> 792 793 <div class="setting-item"> 794 <label class="setting-label"> 795 <span>Save interval</span> 796 <select bind:value={settings.autoSaveInterval} class="select" disabled={!settings.autoSave}> 797 <option value={15000}>15 seconds</option> 798 <option value={30000}>30 seconds</option> 799 <option value={60000}>1 minute</option> 800 <option value={300000}>5 minutes</option> 801 </select> 802 </label> 803 </div> 804 </section> 805 806 <section class="settings-section"> 807 <h2 class="section-title">Window Management</h2> 808 <div class="setting-item"> 809 <label class="setting-label"> 810 <input 811 type="checkbox" 812 bind:checked={settings.showOrphanPrompt} 813 class="checkbox" 814 /> 815 <span>Prompt when new windows are opened</span> 816 </label> 817 <p class="setting-description"> 818 Ask whether to add unassigned windows to the current context 819 </p> 820 </div> 821 822 <div class="setting-item"> 823 <label class="setting-label"> 824 <input 825 type="checkbox" 826 bind:checked={settings.confirmContextSwitch} 827 class="checkbox" 828 /> 829 <span>Confirm before context switch</span> 830 </label> 831 <p class="setting-description"> 832 Show a confirmation dialog before closing windows during context switch 833 </p> 834 </div> 835 836 <div class="setting-item"> 837 <label class="setting-label"> 838 <input 839 type="checkbox" 840 bind:checked={settings.restoreWindowPositions} 841 class="checkbox" 842 /> 843 <span>Restore window positions</span> 844 </label> 845 <p class="setting-description"> 846 Restore window size and position when opening workspaces 847 </p> 848 </div> 849 850 <div class="setting-item"> 851 <label class="setting-label"> 852 <input 853 type="checkbox" 854 bind:checked={settings.useSmartPositioning} 855 class="checkbox" 856 disabled={!settings.restoreWindowPositions} 857 /> 858 <span>Multi-monitor aware positioning</span> 859 </label> 860 <p class="setting-description"> 861 Adapt window positions when monitor configuration changes 862 </p> 863 </div> 864 865 <div class="setting-item"> 866 <label class="setting-label"> 867 <input 868 type="checkbox" 869 bind:checked={settings.lazyLoadTabs} 870 class="checkbox" 871 /> 872 <span>Lazy load tabs when restoring workspaces</span> 873 </label> 874 <p class="setting-description"> 875 Only load the active tab initially; other tabs load when clicked. Reduces memory and CPU usage. 876 </p> 877 </div> 878 </section> 879 880 <section class="settings-section"> 881 <h2 class="section-title">Appearance</h2> 882 <div class="setting-item"> 883 <label class="setting-label" for="theme-select"> 884 <span>Theme</span> 885 <select id="theme-select" bind:value={settings.theme} class="select"> 886 <option value="system">System default</option> 887 <option value="light">Light</option> 888 <option value="dark">Dark</option> 889 </select> 890 </label> 891 <p class="setting-description"> 892 Choose the color theme for the extension 893 </p> 894 </div> 895 </section> 896 897 <section class="settings-section"> 898 <h2 class="section-title">Sync Backend</h2> 899 <div class="setting-item"> 900 <label class="setting-label"> 901 <span>Synchronization method</span> 902 <select bind:value={settings.syncBackend} class="select" onchange={handleSyncBackendChange}> 903 <option value="none">None (local only)</option> 904 <option value="filesystem" disabled={!filesystemSupported}> 905 File System {!filesystemSupported ? '(Not Supported)' : ''} 906 </option> 907 <option value="native">Native Messaging (Syncthing)</option> 908 </select> 909 </label> 910 <p class="setting-description"> 911 Choose how to sync workspaces across devices 912 </p> 913 </div> 914 915 {#if settings.syncBackend === 'filesystem'} 916 <div class="setting-item filesystem-config"> 917 <div class="filesystem-status"> 918 <span class="status-label">Status:</span> 919 {#if filesystemConnected} 920 <span class="status-badge status-active">Connected</span> 921 {:else} 922 <span class="status-badge status-inactive">Not Connected</span> 923 {/if} 924 </div> 925 926 {#if filesystemDirectory} 927 <div class="filesystem-folder"> 928 <span class="folder-label">Sync folder:</span> 929 <span class="folder-path">{filesystemDirectory}</span> 930 </div> 931 {/if} 932 933 <div class="filesystem-actions"> 934 <button 935 type="button" 936 class="btn-primary" 937 onclick={handleSelectDirectory} 938 disabled={selectingDirectory} 939 > 940 {selectingDirectory ? 'Selecting...' : filesystemDirectory ? 'Change Directory' : 'Select Directory'} 941 </button> 942 </div> 943 944 {#if filesystemError} 945 <p class="filesystem-error">{filesystemError}</p> 946 {/if} 947 948 <div class="filesystem-instructions"> 949 <p class="setting-description"> 950 Select a folder to store your workspace data. Use a cloud-synced folder 951 (Dropbox, Google Drive, OneDrive) to sync across devices. 952 </p> 953 <p class="setting-description note"> 954 <strong>Note:</strong> Permission is usually remembered. Re-granting may be needed if you clear browser data. 955 </p> 956 </div> 957 </div> 958 {/if} 959 960 {#if settings.syncBackend === 'native'} 961 <div class="setting-item native-config"> 962 <div class="native-status"> 963 <span class="status-label">Status:</span> 964 {#if nativeTesting} 965 <span class="status-badge status-testing">Testing...</span> 966 {:else if nativeConnected} 967 <span class="status-badge status-active">Connected</span> 968 {:else} 969 <span class="status-badge status-inactive">Not Connected</span> 970 {/if} 971 </div> 972 973 {#if nativeConnected} 974 <div class="native-info"> 975 <div class="info-row"> 976 <span class="info-label">Config Directory:</span> 977 <code class="info-value">{nativeConfigDir || 'Unknown'}</code> 978 </div> 979 <div class="info-row"> 980 <span class="info-label">Last Sync:</span> 981 <span class="info-value">{formatLastSync(nativeLastSync)}</span> 982 </div> 983 </div> 984 985 <div class="native-actions"> 986 <button 987 class="btn btn-secondary" 988 onclick={checkNativeConnection} 989 disabled={nativeTesting} 990 > 991 {nativeTesting ? 'Testing...' : 'Test Connection'} 992 </button> 993 <button 994 class="btn btn-primary" 995 onclick={handleSyncNow} 996 disabled={nativeSyncing} 997 > 998 {nativeSyncing ? 'Syncing...' : 'Sync Now'} 999 </button> 1000 </div> 1001 {:else} 1002 <div class="native-actions"> 1003 <button 1004 class="btn btn-primary" 1005 onclick={checkNativeConnection} 1006 disabled={nativeTesting} 1007 > 1008 {nativeTesting ? 'Connecting...' : 'Connect'} 1009 </button> 1010 </div> 1011 {/if} 1012 1013 {#if nativeError} 1014 <div class="native-error"> 1015 <svg class="error-icon" fill="currentColor" viewBox="0 0 20 20"> 1016 <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> 1017 </svg> 1018 <span>{nativeError}</span> 1019 </div> 1020 {/if} 1021 1022 <div class="native-instructions"> 1023 <h4>Setup Instructions:</h4> 1024 <ol> 1025 <li> 1026 <strong>Install the native host:</strong> Run <code>install_windows.bat</code> (Windows) 1027 or <code>install_macos.sh</code> (macOS/Linux) from the <code>native-host</code> folder. 1028 </li> 1029 <li> 1030 <strong>Configure Syncthing folder:</strong> The extension syncs to 1031 <code>~/Syncthing/MnemonicWorkspaces/</code> by default. Create this folder 1032 and add it to Syncthing. 1033 </li> 1034 <li> 1035 <strong>Reload extension:</strong> After installing the native host, reload 1036 this extension from <code>chrome://extensions</code> or <code>about:debugging</code>. 1037 </li> 1038 <li> 1039 <strong>Click Connect:</strong> The extension will communicate with the native 1040 host to read/write the backup file. 1041 </li> 1042 </ol> 1043 <p class="setup-note"> 1044 <strong>How it works:</strong> When you click "Sync Now", all your workspaces are 1045 saved to <code>workspaces.json</code> in the Syncthing folder. Syncthing then 1046 syncs this file to your other devices. On other devices, use "Check for Changes" 1047 to import updates. 1048 </p> 1049 </div> 1050 </div> 1051 {/if} 1052 </section> 1053 1054 <section class="settings-section"> 1055 <h2 class="section-title">Claude AI Integration</h2> 1056 <p class="section-description"> 1057 Enable AI-powered workspace suggestions using Claude. 1058 </p> 1059 1060 <div class="setting-item"> 1061 <div class="api-key-status"> 1062 <span class="status-label">Status:</span> 1063 {#if claudeEnabled} 1064 <span class="status-badge status-active">Configured</span> 1065 {:else} 1066 <span class="status-badge status-inactive">Not configured</span> 1067 {/if} 1068 </div> 1069 </div> 1070 1071 {#if !claudeEnabled} 1072 <div class="setting-item"> 1073 <label class="setting-label" for="claude-api-key"> 1074 <span>API Key</span> 1075 </label> 1076 <div class="api-key-input"> 1077 <input 1078 id="claude-api-key" 1079 type="password" 1080 bind:value={claudeApiKey} 1081 placeholder="sk-ant-..." 1082 class="input" 1083 /> 1084 <button 1085 class="btn-secondary" 1086 onclick={saveClaudeApiKey} 1087 disabled={!claudeApiKey.trim()} 1088 > 1089 Save Key 1090 </button> 1091 </div> 1092 <p class="setting-description"> 1093 Get your API key from <a href="https://console.anthropic.com" target="_blank" rel="noopener noreferrer">console.anthropic.com</a> 1094 </p> 1095 </div> 1096 {:else} 1097 <div class="setting-item"> 1098 <div class="api-key-actions"> 1099 <button 1100 class="btn-secondary" 1101 onclick={testClaudeConnection} 1102 disabled={claudeTesting} 1103 > 1104 {#if claudeTesting} 1105 Testing... 1106 {:else if claudeTestResult === 'success'} 1107 Connected! 1108 {:else if claudeTestResult === 'error'} 1109 Failed 1110 {:else} 1111 Test Connection 1112 {/if} 1113 </button> 1114 <button class="btn-danger" onclick={clearClaudeApiKey}> 1115 Remove API Key 1116 </button> 1117 </div> 1118 {#if claudeTestResult === 'error' && claudeTestError} 1119 <p class="setting-description" style="color: #EF4444; margin-top: 4px;"> 1120 {claudeTestError} 1121 </p> 1122 {/if} 1123 </div> 1124 1125 <div class="setting-item"> 1126 <label class="setting-label"> 1127 <input 1128 type="checkbox" 1129 bind:checked={claudeAutoSuggest} 1130 onchange={updateClaudeAutoSuggest} 1131 class="checkbox" 1132 /> 1133 <span>Auto-suggest improvements</span> 1134 </label> 1135 <p class="setting-description"> 1136 Automatically analyze workspaces and suggest organizational improvements 1137 </p> 1138 </div> 1139 {/if} 1140 </section> 1141 1142 <section class="settings-section pause-media-section"> 1143 <h2 class="section-title">Pause Media Sites</h2> 1144 <p class="section-description"> 1145 Configure which sites the "Pause Audio" feature applies to. Default sites include YouTube, Vimeo, Twitch, Netflix, Spotify, and more. 1146 </p> 1147 1148 <div class="setting-item"> 1149 <div class="pause-media-status"> 1150 <span class="status-label">Default sites:</span> 1151 <span class="site-count">{DEFAULT_PAUSE_MEDIA_SITES.length} sites</span> 1152 {#if (settings.pauseMediaSites?.length || 0) > 0} 1153 <span class="divider">|</span> 1154 <span class="status-label">Custom sites:</span> 1155 <span class="site-count custom">{settings.pauseMediaSites?.length || 0} sites</span> 1156 {/if} 1157 </div> 1158 </div> 1159 1160 {#if !pauseMediaEditMode} 1161 <div class="setting-item"> 1162 <div class="pause-media-preview"> 1163 <div class="sites-list"> 1164 <strong>Default sites:</strong> 1165 <span class="sites-preview">YouTube, Vimeo, Twitch, Netflix, Hulu, Disney+, Prime Video, HBO Max, Spotify, SoundCloud, Bandcamp, Tidal, Deezer, Pandora, Invidious (public instances)</span> 1166 </div> 1167 {#if (settings.pauseMediaSites?.length || 0) > 0} 1168 <div class="sites-list custom-list"> 1169 <strong>Custom sites:</strong> 1170 <span class="sites-preview">{settings.pauseMediaSites?.join(', ')}</span> 1171 </div> 1172 {/if} 1173 </div> 1174 </div> 1175 1176 <div class="setting-item"> 1177 <div class="pause-media-actions"> 1178 <button class="btn-secondary" onclick={startEditPauseMediaSites}> 1179 Edit Sites 1180 </button> 1181 {#if (settings.pauseMediaSites?.length || 0) > 0} 1182 <button class="btn-danger-outline" onclick={resetPauseMediaToDefaults}> 1183 Reset to Defaults 1184 </button> 1185 {/if} 1186 {#if pauseMediaSaved} 1187 <span class="save-indicator">Saved!</span> 1188 {/if} 1189 </div> 1190 </div> 1191 {:else} 1192 <div class="setting-item pause-media-editor"> 1193 <label class="setting-label" for="pause-media-textarea"> 1194 <span>Edit site patterns</span> 1195 </label> 1196 <textarea 1197 id="pause-media-textarea" 1198 class="sites-textarea" 1199 bind:value={pauseMediaText} 1200 rows="20" 1201 placeholder="Enter site patterns, one per line..." 1202 ></textarea> 1203 <p class="setting-description"> 1204 Use match patterns like <code>*://*.example.com/*</code> or <code>*://localhost:8096/*</code> 1205 <br /> 1206 Lines starting with <code>#</code> are comments. Custom sites are saved separately from defaults. 1207 </p> 1208 <div class="pause-media-actions"> 1209 <button class="btn-primary" onclick={savePauseMediaSites}> 1210 Save Changes 1211 </button> 1212 <button class="btn-secondary" onclick={cancelEditPauseMediaSites}> 1213 Cancel 1214 </button> 1215 </div> 1216 </div> 1217 {/if} 1218 1219 <div class="setting-item pause-media-info"> 1220 <p class="setting-description note"> 1221 <strong>Note:</strong> Default sites use pre-injected content scripts for instant response. 1222 Custom sites (especially those with ports like <code>:3000</code>) work via programmatic injection when you click "Pause Audio". 1223 </p> 1224 </div> 1225 </section> 1226 1227 <section class="settings-section"> 1228 <h2 class="section-title">Logseq Integration</h2> 1229 <p class="section-description"> 1230 Sync workspace notes with Logseq for a connected knowledge graph. 1231 </p> 1232 1233 <div class="setting-item"> 1234 <label class="setting-label"> 1235 <input 1236 type="checkbox" 1237 checked={logseqSettings.enabled} 1238 onchange={toggleLogseqEnabled} 1239 class="checkbox" 1240 /> 1241 <span>Enable Logseq Notes</span> 1242 </label> 1243 <p class="setting-description"> 1244 Store workspace notes in Logseq. Requires Logseq to be running with HTTP API enabled. 1245 </p> 1246 </div> 1247 1248 {#if logseqSettings.enabled} 1249 <div class="setting-item"> 1250 <div class="api-key-status"> 1251 <span class="status-label">Status:</span> 1252 {#if logseqConnected} 1253 <span class="status-badge status-active">Connected to {logseqGraphName}</span> 1254 {:else} 1255 <span class="status-badge status-inactive">Not connected</span> 1256 {/if} 1257 </div> 1258 </div> 1259 1260 <div class="setting-item"> 1261 <label class="setting-label" for="logseq-api-url"> 1262 <span>API URL</span> 1263 </label> 1264 <div class="api-key-input"> 1265 <input 1266 id="logseq-api-url" 1267 type="text" 1268 bind:value={logseqSettings.apiUrl} 1269 placeholder="http://127.0.0.1:12315/api" 1270 class="input" 1271 /> 1272 <button class="btn-secondary" onclick={saveLogseqSettings}> 1273 Save 1274 </button> 1275 </div> 1276 <p class="setting-description"> 1277 Default: http://127.0.0.1:12315/api (Logseq's default HTTP API port) 1278 </p> 1279 </div> 1280 1281 <div class="setting-item"> 1282 <label class="setting-label" for="logseq-token"> 1283 <span>Auth Token (optional)</span> 1284 </label> 1285 <div class="api-key-input"> 1286 <input 1287 id="logseq-token" 1288 type="password" 1289 bind:value={logseqSettings.authToken} 1290 placeholder="Leave empty if not using authentication" 1291 class="input" 1292 /> 1293 <button class="btn-secondary" onclick={saveLogseqSettings}> 1294 Save 1295 </button> 1296 </div> 1297 <p class="setting-description"> 1298 Only needed if you've configured Logseq API authentication. 1299 </p> 1300 </div> 1301 1302 <div class="setting-item"> 1303 <div class="api-key-actions"> 1304 <button 1305 class="btn-secondary" 1306 onclick={testLogseqConnection} 1307 disabled={logseqTesting} 1308 > 1309 {#if logseqTesting} 1310 Testing... 1311 {:else if logseqTestResult === 'success'} 1312 Connected! 1313 {:else if logseqTestResult === 'error'} 1314 Failed 1315 {:else} 1316 Test Connection 1317 {/if} 1318 </button> 1319 </div> 1320 </div> 1321 1322 <div class="setting-item"> 1323 <label class="setting-label"> 1324 <input 1325 type="checkbox" 1326 bind:checked={logseqSettings.autoSync} 1327 onchange={saveLogseqSettings} 1328 class="checkbox" 1329 /> 1330 <span>Auto-sync notes</span> 1331 </label> 1332 <p class="setting-description"> 1333 Automatically sync notes with Logseq when changes are made 1334 </p> 1335 </div> 1336 1337 <!-- Graph Access for File Attachments --> 1338 {#if graphAccessSupported} 1339 <div class="setting-item graph-access-section"> 1340 <h4 class="subsection-title">Graph Access for Attachments</h4> 1341 <p class="setting-description"> 1342 Grant access to your Logseq graph folder to store file attachments directly. 1343 This allows you to drag and drop files into workspace resources. 1344 </p> 1345 1346 <div class="graph-access-status"> 1347 <div class="api-key-status"> 1348 <span class="status-label">Status:</span> 1349 {#if graphAccessGranted} 1350 <span class="status-badge status-active">Access granted</span> 1351 {:else} 1352 <span class="status-badge status-inactive">No access</span> 1353 {/if} 1354 </div> 1355 1356 {#if graphAccessGranted && graphAccessDirectory} 1357 <div class="graph-directory-info"> 1358 <span class="directory-label">Directory:</span> 1359 <span class="directory-path">{graphAccessDirectory}</span> 1360 </div> 1361 {/if} 1362 </div> 1363 1364 <div class="api-key-actions"> 1365 {#if graphAccessGranted} 1366 <button 1367 class="btn-secondary btn-danger" 1368 onclick={revokeGraphAccess} 1369 > 1370 Revoke Access 1371 </button> 1372 {:else} 1373 <button 1374 class="btn-primary" 1375 onclick={requestGraphAccess} 1376 disabled={graphAccessRequesting} 1377 > 1378 {#if graphAccessRequesting} 1379 Requesting... 1380 {:else} 1381 Grant Access 1382 {/if} 1383 </button> 1384 {/if} 1385 </div> 1386 1387 <p class="setting-description"> 1388 When you click "Grant Access", select your Logseq graph root folder. 1389 Files will be stored in <code>assets/mnemonic/</code> within your graph. 1390 </p> 1391 </div> 1392 {:else} 1393 <div class="setting-item graph-access-section"> 1394 <h4 class="subsection-title">Graph Access for Attachments</h4> 1395 <p class="setting-description warning-text"> 1396 File attachment storage is not supported in this browser. 1397 Use Chrome, Edge, or another Chromium-based browser for full attachment support. 1398 </p> 1399 </div> 1400 {/if} 1401 {/if} 1402 1403 <div class="setting-item logseq-instructions"> 1404 <h4>How to enable Logseq HTTP API:</h4> 1405 <ol> 1406 <li>Open Logseq</li> 1407 <li>Go to Settings (gear icon)</li> 1408 <li>Enable "HTTP APIs server" under Features</li> 1409 <li>Restart Logseq</li> 1410 <li>The API will be available at http://127.0.0.1:12315/api</li> 1411 </ol> 1412 </div> 1413 </section> 1414 1415 <section class="settings-section"> 1416 <h2 class="section-title">Todoist Integration</h2> 1417 <p class="section-description"> 1418 Sync workspace tasks with Todoist for cross-platform task management. 1419 </p> 1420 1421 <div class="setting-item"> 1422 <div class="api-key-status"> 1423 <span class="status-label">Status:</span> 1424 {#if todoistConnection?.connected} 1425 <span class="status-badge status-active">Connected</span> 1426 {:else if todoistEnabled} 1427 <span class="status-badge status-inactive">Disconnected</span> 1428 {:else} 1429 <span class="status-badge status-inactive">Not configured</span> 1430 {/if} 1431 </div> 1432 </div> 1433 1434 {#if todoistConnection?.connected} 1435 <div class="setting-item"> 1436 <div class="todoist-info"> 1437 <div class="info-row"> 1438 <span class="info-label">Master Project:</span> 1439 <span class="info-value">{todoistConnection.masterProjectName}</span> 1440 </div> 1441 {#if todoistConnection.lastSync} 1442 <div class="info-row"> 1443 <span class="info-label">Last Sync:</span> 1444 <span class="info-value">{formatLastSync(todoistConnection.lastSync)}</span> 1445 </div> 1446 {/if} 1447 <div class="info-row"> 1448 <span class="info-label">Auth Method:</span> 1449 <span class="info-value">{todoistConnection.authMethod === 'oauth' ? 'OAuth' : 'API Token'}</span> 1450 </div> 1451 </div> 1452 </div> 1453 1454 <div class="setting-item"> 1455 <div class="api-key-actions"> 1456 <button 1457 class="btn-secondary" 1458 onclick={testTodoistConnection} 1459 disabled={todoistTesting} 1460 > 1461 {#if todoistTesting} 1462 Testing... 1463 {:else if todoistTestResult === 'success'} 1464 Connected! 1465 {:else if todoistTestResult === 'error'} 1466 Failed 1467 {:else} 1468 Test Connection 1469 {/if} 1470 </button> 1471 <button class="btn-danger" onclick={disconnectTodoist}> 1472 Disconnect 1473 </button> 1474 </div> 1475 </div> 1476 {:else if todoistEnabled} 1477 <div class="setting-item"> 1478 <p class="setting-description" style="margin-left: 0; color: var(--text-secondary);"> 1479 API token is saved but connection failed{#if todoistConnection?.error}: {todoistConnection.error}{/if} 1480 </p> 1481 <div class="api-key-actions"> 1482 <button 1483 class="btn-secondary" 1484 onclick={testTodoistConnection} 1485 disabled={todoistTesting} 1486 > 1487 {#if todoistTesting} 1488 Reconnecting... 1489 {:else if todoistTestResult === 'success'} 1490 Connected! 1491 {:else if todoistTestResult === 'error'} 1492 Failed 1493 {:else} 1494 Retry Connection 1495 {/if} 1496 </button> 1497 <button class="btn-danger" onclick={disconnectTodoist}> 1498 Disconnect 1499 </button> 1500 </div> 1501 </div> 1502 {:else} 1503 <div class="setting-item"> 1504 <label class="setting-label" for="todoist-api-token"> 1505 <span>API Token</span> 1506 </label> 1507 <div class="api-key-input"> 1508 <input 1509 id="todoist-api-token" 1510 type="password" 1511 bind:value={todoistApiToken} 1512 placeholder="Enter your Todoist API token" 1513 class="input" 1514 /> 1515 <button 1516 class="btn-secondary" 1517 onclick={saveTodoistApiToken} 1518 disabled={!todoistApiToken.trim() || todoistTesting} 1519 > 1520 {#if todoistTesting} 1521 Connecting... 1522 {:else if todoistTestResult === 'success'} 1523 Connected! 1524 {:else if todoistTestResult === 'error'} 1525 Failed 1526 {:else} 1527 Connect 1528 {/if} 1529 </button> 1530 </div> 1531 <p class="setting-description"> 1532 Get your API token from <a href="https://todoist.com/app/settings/integrations/developer" target="_blank" rel="noopener noreferrer">Todoist Settings > Integrations > Developer</a> 1533 </p> 1534 </div> 1535 {/if} 1536 1537 <div class="setting-item todoist-instructions"> 1538 <h4>How it works:</h4> 1539 <ol> 1540 <li>A "Mnemonic" project is created in your Todoist account</li> 1541 <li>Each workspace becomes a sub-project under Mnemonic</li> 1542 <li>Child workspaces become sub-projects of their parent</li> 1543 <li>Tasks sync automatically between the extension and Todoist</li> 1544 </ol> 1545 <p class="setup-note"> 1546 <strong>Tip:</strong> Tasks created in Todoist will appear in the workspace dashboard. 1547 Use Todoist's mobile app to manage tasks on the go! 1548 </p> 1549 </div> 1550 </section> 1551 1552 <!-- Security Section --> 1553 <section class="settings-section"> 1554 <h2 class="section-title"> 1555 Security 1556 {#if vaultUnlocked} 1557 <span class="status-badge success">Unlocked</span> 1558 {:else if vaultExists} 1559 <span class="status-badge warning">Locked</span> 1560 {/if} 1561 </h2> 1562 1563 <div class="setting-item"> 1564 <p class="setting-description" style="margin-left: 0; margin-bottom: 1rem;"> 1565 Encrypt your integration credentials (API keys) with a master password. 1566 Encrypted credentials sync securely across devices via the sync daemon. 1567 </p> 1568 1569 {#if vaultSuccess} 1570 <div class="success-message"> 1571 <svg viewBox="0 0 20 20" fill="currentColor"> 1572 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> 1573 </svg> 1574 <span>{vaultSuccess}</span> 1575 </div> 1576 {/if} 1577 1578 {#if vaultError} 1579 <div class="error-message"> 1580 <svg viewBox="0 0 20 20" fill="currentColor"> 1581 <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> 1582 </svg> 1583 <span>{vaultError}</span> 1584 </div> 1585 {/if} 1586 </div> 1587 1588 {#if !nativeConnected && settings.syncBackend !== 'native'} 1589 <div class="setting-item"> 1590 <div class="warning-box"> 1591 <svg viewBox="0 0 20 20" fill="currentColor"> 1592 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> 1593 </svg> 1594 <span>Enable Native Messaging Sync to use encrypted credential storage.</span> 1595 </div> 1596 </div> 1597 {:else if !vaultExists && !vaultSetupMode} 1598 <!-- No vault exists - show setup prompt --> 1599 <div class="setting-item"> 1600 <div class="vault-setup-prompt"> 1601 <div class="shield-icon"> 1602 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1603 <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> 1604 <path d="M9 12l2 2 4-4"/> 1605 </svg> 1606 </div> 1607 <div class="vault-prompt-text"> 1608 <strong>Secure your integration credentials</strong> 1609 <p>Create a master password to encrypt API keys for Claude, Todoist, and Logseq.</p> 1610 </div> 1611 <button class="btn-primary" onclick={() => (vaultSetupMode = true)}> 1612 Setup Encryption 1613 </button> 1614 </div> 1615 </div> 1616 {:else if vaultSetupMode} 1617 <!-- Setting up new vault --> 1618 <div class="setting-item"> 1619 <div class="vault-setup-form"> 1620 <div class="input-group"> 1621 <label for="vault-new-password">Master Password</label> 1622 <input 1623 id="vault-new-password" 1624 type="password" 1625 bind:value={vaultNewPassword} 1626 placeholder="Create a strong password" 1627 class="text-input" 1628 /> 1629 </div> 1630 <div class="input-group"> 1631 <label for="vault-confirm-password">Confirm Password</label> 1632 <input 1633 id="vault-confirm-password" 1634 type="password" 1635 bind:value={vaultConfirmPassword} 1636 placeholder="Confirm your password" 1637 class="text-input" 1638 /> 1639 </div> 1640 <div class="warning-box"> 1641 <svg viewBox="0 0 20 20" fill="currentColor"> 1642 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> 1643 </svg> 1644 <span>This password cannot be recovered. Keep it safe!</span> 1645 </div> 1646 <div class="button-row"> 1647 <button class="btn-secondary" onclick={() => { vaultSetupMode = false; vaultNewPassword = ''; vaultConfirmPassword = ''; vaultError = null; }}> 1648 Cancel 1649 </button> 1650 <button 1651 class="btn-primary" 1652 onclick={handleSetupVault} 1653 disabled={!vaultNewPassword || !vaultConfirmPassword || vaultNewPassword !== vaultConfirmPassword} 1654 > 1655 Create Master Password 1656 </button> 1657 </div> 1658 </div> 1659 </div> 1660 {:else if vaultExists && vaultUnlocked && !vaultChangingPassword} 1661 <!-- Vault is unlocked - show status and actions --> 1662 <div class="setting-item"> 1663 <div class="vault-status"> 1664 <div class="vault-status-header"> 1665 <div class="vault-icon unlocked"> 1666 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1667 <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/> 1668 <path d="M7 11V7a5 5 0 0 1 9.9-1"/> 1669 </svg> 1670 </div> 1671 <div class="vault-status-text"> 1672 <strong>Credential Vault Unlocked</strong> 1673 <p>Your integration credentials are encrypted and synced.</p> 1674 </div> 1675 </div> 1676 <div class="vault-credentials-list"> 1677 <span class="cred-item {vaultCredentials.claude ? 'stored' : 'empty'}"> 1678 Claude {vaultCredentials.claude ? '(stored)' : '(not set)'} 1679 </span> 1680 <span class="cred-item {vaultCredentials.todoist ? 'stored' : 'empty'}"> 1681 Todoist {vaultCredentials.todoist ? '(stored)' : '(not set)'} 1682 </span> 1683 <span class="cred-item {vaultCredentials.logseq ? 'stored' : 'empty'}"> 1684 Logseq {vaultCredentials.logseq ? '(stored)' : '(not set)'} 1685 </span> 1686 </div> 1687 <div class="vault-actions"> 1688 <button class="btn-secondary" onclick={() => (vaultChangingPassword = true)}> 1689 Change Password 1690 </button> 1691 <button class="btn-secondary" onclick={handleLockVault}> 1692 Lock Vault 1693 </button> 1694 </div> 1695 </div> 1696 </div> 1697 {:else if vaultExists && !vaultUnlocked} 1698 <!-- Vault exists but locked --> 1699 <div class="setting-item"> 1700 <div class="vault-locked"> 1701 <div class="vault-icon locked"> 1702 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1703 <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/> 1704 <path d="M7 11V7a5 5 0 0 1 10 0v4"/> 1705 </svg> 1706 </div> 1707 <div class="vault-locked-text"> 1708 <strong>Credential Vault Locked</strong> 1709 <p>Enter your master password to unlock and access your encrypted credentials.</p> 1710 </div> 1711 <div class="input-group"> 1712 <input 1713 type="password" 1714 bind:value={vaultOldPassword} 1715 placeholder="Enter master password" 1716 class="text-input" 1717 onkeydown={(e) => e.key === 'Enter' && handleUnlockVault()} 1718 /> 1719 <button class="btn-primary" onclick={handleUnlockVault} disabled={!vaultOldPassword}> 1720 Unlock 1721 </button> 1722 </div> 1723 </div> 1724 </div> 1725 {:else if vaultChangingPassword} 1726 <!-- Changing password --> 1727 <div class="setting-item"> 1728 <div class="vault-setup-form"> 1729 <h4>Change Master Password</h4> 1730 <div class="input-group"> 1731 <label for="vault-old-password">Current Password</label> 1732 <input 1733 id="vault-old-password" 1734 type="password" 1735 bind:value={vaultOldPassword} 1736 placeholder="Enter current password" 1737 class="text-input" 1738 /> 1739 </div> 1740 <div class="input-group"> 1741 <label for="vault-new-password-change">New Password</label> 1742 <input 1743 id="vault-new-password-change" 1744 type="password" 1745 bind:value={vaultNewPassword} 1746 placeholder="Enter new password" 1747 class="text-input" 1748 /> 1749 </div> 1750 <div class="input-group"> 1751 <label for="vault-confirm-password-change">Confirm New Password</label> 1752 <input 1753 id="vault-confirm-password-change" 1754 type="password" 1755 bind:value={vaultConfirmPassword} 1756 placeholder="Confirm new password" 1757 class="text-input" 1758 /> 1759 </div> 1760 <div class="button-row"> 1761 <button class="btn-secondary" onclick={() => { vaultChangingPassword = false; vaultOldPassword = ''; vaultNewPassword = ''; vaultConfirmPassword = ''; vaultError = null; }}> 1762 Cancel 1763 </button> 1764 <button 1765 class="btn-primary" 1766 onclick={handleChangeVaultPassword} 1767 disabled={!vaultOldPassword || !vaultNewPassword || vaultNewPassword !== vaultConfirmPassword} 1768 > 1769 Change Password 1770 </button> 1771 </div> 1772 </div> 1773 </div> 1774 {/if} 1775 1776 <div class="setting-item security-info"> 1777 <h4>How credential encryption works:</h4> 1778 <ul> 1779 <li>Your master password is never stored - only you know it</li> 1780 <li>A 256-bit encryption key is derived using PBKDF2</li> 1781 <li>Credentials are encrypted with AES-256-GCM</li> 1782 <li>Encrypted data syncs via daemon - decryptable only with your password</li> 1783 <li>On new devices, just enter your master password to restore credentials</li> 1784 </ul> 1785 </div> 1786 </section> 1787 </main> 1788 1789 <footer class="options-footer"> 1790 <button class="btn-primary" onclick={saveSettings} disabled={saving}> 1791 {#if saving} 1792 Saving... 1793 {:else if saved} 1794 Saved! 1795 {:else} 1796 Save Settings 1797 {/if} 1798 </button> 1799 </footer> 1800 </div> 1801 1802 <style> 1803 .options { 1804 max-width: 960px; 1805 margin: 0 auto; 1806 padding: 2rem 3rem; 1807 min-height: 100vh; 1808 display: flex; 1809 flex-direction: column; 1810 } 1811 1812 .options-header { 1813 display: flex; 1814 align-items: center; 1815 gap: 1rem; 1816 margin-bottom: 2.5rem; 1817 padding-bottom: 1.5rem; 1818 border-bottom: 1px solid rgba(217, 137, 46, 0.2); 1819 } 1820 1821 .logo-mark { 1822 width: 48px; 1823 height: 48px; 1824 display: flex; 1825 align-items: center; 1826 justify-content: center; 1827 background-color: var(--color-phosphor); 1828 color: var(--color-bg); 1829 font-weight: bold; 1830 font-size: 1.5rem; 1831 border-radius: 10px; 1832 } 1833 1834 .options-title { 1835 font-size: 1.75rem; 1836 color: var(--color-phosphor); 1837 margin: 0; 1838 } 1839 1840 .options-content { 1841 flex: 1; 1842 display: grid; 1843 grid-template-columns: repeat(2, 1fr); 1844 gap: 1.5rem; 1845 } 1846 1847 /* Make Claude AI and Logseq sections span full width */ 1848 .options-content .settings-section:nth-last-child(-n+2) { 1849 grid-column: 1 / -1; 1850 } 1851 1852 .settings-section { 1853 padding: 1.5rem; 1854 background-color: var(--color-bg-light); 1855 border-radius: 8px; 1856 border: 1px solid rgba(217, 137, 46, 0.15); 1857 height: fit-content; 1858 } 1859 1860 .section-title { 1861 font-size: 1.125rem; 1862 color: var(--color-phosphor); 1863 margin: 0 0 1rem 0; 1864 padding-bottom: 0.5rem; 1865 border-bottom: 1px solid rgba(217, 137, 46, 0.2); 1866 } 1867 1868 .setting-item { 1869 margin-bottom: 1.25rem; 1870 } 1871 1872 .setting-item:last-child { 1873 margin-bottom: 0; 1874 } 1875 1876 .setting-label { 1877 display: flex; 1878 align-items: center; 1879 gap: 0.75rem; 1880 color: var(--color-text-primary); 1881 cursor: pointer; 1882 } 1883 1884 .setting-description { 1885 font-size: 0.875rem; 1886 color: var(--color-text-muted); 1887 margin: 0.25rem 0 0 1.75rem; 1888 } 1889 1890 .checkbox { 1891 width: 18px; 1892 height: 18px; 1893 accent-color: var(--color-phosphor); 1894 } 1895 1896 .select { 1897 padding: 0.5rem 0.75rem; 1898 background-color: var(--color-bg-dark); 1899 color: var(--color-text-primary); 1900 border: 1px solid rgba(217, 137, 46, 0.3); 1901 border-radius: 4px; 1902 cursor: pointer; 1903 } 1904 1905 .select:focus { 1906 outline: none; 1907 border-color: var(--color-phosphor); 1908 } 1909 1910 .select:disabled { 1911 opacity: 0.5; 1912 cursor: not-allowed; 1913 } 1914 1915 .options-footer { 1916 margin-top: 2rem; 1917 padding-top: 1.5rem; 1918 border-top: 1px solid rgba(217, 137, 46, 0.2); 1919 display: flex; 1920 justify-content: flex-end; 1921 } 1922 1923 .btn-primary { 1924 min-width: 200px; 1925 padding: 0.875rem 2rem; 1926 background-color: var(--color-phosphor); 1927 color: var(--color-bg); 1928 border: none; 1929 border-radius: 6px; 1930 font-weight: 600; 1931 font-size: 1rem; 1932 cursor: pointer; 1933 transition: background-color 0.2s; 1934 } 1935 1936 .btn-primary:hover:not(:disabled) { 1937 background-color: var(--color-phosphor-light); 1938 } 1939 1940 .btn-primary:disabled { 1941 opacity: 0.7; 1942 cursor: not-allowed; 1943 } 1944 1945 .section-description { 1946 font-size: 0.875rem; 1947 color: var(--color-text-secondary); 1948 margin-bottom: 1rem; 1949 } 1950 1951 .api-key-status { 1952 display: flex; 1953 align-items: center; 1954 gap: 0.5rem; 1955 } 1956 1957 .status-label { 1958 color: var(--color-text-secondary); 1959 font-size: 0.875rem; 1960 } 1961 1962 .status-badge { 1963 padding: 0.25rem 0.5rem; 1964 border-radius: 4px; 1965 font-size: 0.75rem; 1966 font-weight: 500; 1967 text-transform: uppercase; 1968 letter-spacing: 0.05em; 1969 } 1970 1971 .status-active { 1972 background: rgba(46, 160, 67, 0.2); 1973 color: #2ea043; 1974 border: 1px solid rgba(46, 160, 67, 0.3); 1975 } 1976 1977 .status-inactive { 1978 background: rgba(217, 137, 46, 0.1); 1979 color: var(--color-text-muted); 1980 border: 1px solid rgba(217, 137, 46, 0.2); 1981 } 1982 1983 .api-key-input { 1984 display: flex; 1985 gap: 0.5rem; 1986 margin-top: 0.5rem; 1987 } 1988 1989 .input { 1990 flex: 1; 1991 padding: 0.5rem 0.75rem; 1992 background-color: var(--color-bg-dark); 1993 color: var(--color-text-primary); 1994 border: 1px solid rgba(217, 137, 46, 0.3); 1995 border-radius: 4px; 1996 font-family: var(--font-mono); 1997 } 1998 1999 .input:focus { 2000 outline: none; 2001 border-color: var(--color-phosphor); 2002 } 2003 2004 .input::placeholder { 2005 color: var(--color-text-muted); 2006 } 2007 2008 .btn-secondary { 2009 padding: 0.5rem 1rem; 2010 background-color: transparent; 2011 color: var(--color-phosphor); 2012 border: 1px solid var(--color-phosphor); 2013 border-radius: 4px; 2014 font-weight: 500; 2015 cursor: pointer; 2016 transition: background-color 0.2s; 2017 white-space: nowrap; 2018 } 2019 2020 .btn-secondary:hover:not(:disabled) { 2021 background-color: rgba(217, 137, 46, 0.1); 2022 } 2023 2024 .btn-secondary:disabled { 2025 opacity: 0.5; 2026 cursor: not-allowed; 2027 } 2028 2029 .btn-danger { 2030 padding: 0.5rem 1rem; 2031 background-color: transparent; 2032 color: var(--color-status-error); 2033 border: 1px solid var(--color-status-error); 2034 border-radius: 4px; 2035 font-weight: 500; 2036 cursor: pointer; 2037 transition: background-color 0.2s; 2038 } 2039 2040 .btn-danger:hover { 2041 background-color: rgba(218, 54, 51, 0.1); 2042 } 2043 2044 .api-key-actions { 2045 display: flex; 2046 gap: 0.75rem; 2047 } 2048 2049 .setting-description a { 2050 color: var(--color-phosphor); 2051 text-decoration: underline; 2052 } 2053 2054 .setting-description a:hover { 2055 color: var(--color-phosphor-light); 2056 } 2057 2058 /* Filesystem sync styles */ 2059 .filesystem-config { 2060 margin-top: 1rem; 2061 padding: 1rem; 2062 background: rgba(0, 0, 0, 0.2); 2063 border-radius: 6px; 2064 } 2065 2066 .filesystem-status { 2067 display: flex; 2068 align-items: center; 2069 gap: 0.5rem; 2070 margin-bottom: 0.75rem; 2071 } 2072 2073 .filesystem-folder { 2074 margin-bottom: 0.75rem; 2075 padding: 0.5rem 0.75rem; 2076 background: rgba(0, 0, 0, 0.2); 2077 border-radius: 4px; 2078 font-size: 0.875rem; 2079 } 2080 2081 .folder-label { 2082 color: var(--color-text-secondary); 2083 margin-right: 0.5rem; 2084 } 2085 2086 .folder-path { 2087 color: var(--color-phosphor); 2088 font-family: var(--font-mono); 2089 } 2090 2091 .filesystem-actions { 2092 margin-bottom: 0.75rem; 2093 } 2094 2095 .filesystem-error { 2096 color: var(--color-status-error); 2097 font-size: 0.875rem; 2098 margin: 0.5rem 0; 2099 } 2100 2101 .filesystem-instructions { 2102 margin-top: 0.5rem; 2103 } 2104 2105 .filesystem-instructions .note { 2106 margin-top: 0.5rem; 2107 padding: 0.5rem 0.75rem; 2108 background: rgba(217, 137, 46, 0.1); 2109 border-radius: 4px; 2110 color: var(--color-phosphor); 2111 } 2112 2113 /* Native messaging sync styles */ 2114 .native-config { 2115 margin-top: 1rem; 2116 padding: 1rem; 2117 background: rgba(0, 0, 0, 0.2); 2118 border-radius: 6px; 2119 } 2120 2121 .native-status { 2122 display: flex; 2123 align-items: center; 2124 gap: 0.5rem; 2125 margin-bottom: 0.75rem; 2126 } 2127 2128 .native-info { 2129 margin-bottom: 0.75rem; 2130 padding: 0.75rem; 2131 background: rgba(0, 0, 0, 0.2); 2132 border-radius: 4px; 2133 } 2134 2135 .info-row { 2136 display: flex; 2137 align-items: center; 2138 gap: 0.5rem; 2139 margin-bottom: 0.375rem; 2140 } 2141 2142 .info-row:last-child { 2143 margin-bottom: 0; 2144 } 2145 2146 .info-label { 2147 color: var(--color-text-secondary); 2148 font-size: 0.875rem; 2149 min-width: 110px; 2150 } 2151 2152 .info-value { 2153 color: var(--color-text-primary); 2154 font-size: 0.875rem; 2155 } 2156 2157 code.info-value { 2158 color: var(--color-phosphor); 2159 font-family: var(--font-mono); 2160 font-size: 0.8125rem; 2161 background: rgba(217, 137, 46, 0.1); 2162 padding: 0.125rem 0.375rem; 2163 border-radius: 3px; 2164 } 2165 2166 .native-actions { 2167 display: flex; 2168 gap: 0.5rem; 2169 margin-bottom: 0.75rem; 2170 } 2171 2172 .native-error { 2173 display: flex; 2174 align-items: center; 2175 gap: 0.5rem; 2176 color: var(--color-status-error); 2177 font-size: 0.875rem; 2178 margin-bottom: 0.75rem; 2179 padding: 0.5rem 0.75rem; 2180 background: rgba(239, 68, 68, 0.1); 2181 border-radius: 4px; 2182 } 2183 2184 .native-error .error-icon { 2185 width: 1rem; 2186 height: 1rem; 2187 flex-shrink: 0; 2188 } 2189 2190 .native-instructions { 2191 margin-top: 0.75rem; 2192 padding: 1rem; 2193 background: rgba(217, 137, 46, 0.05); 2194 border-radius: 6px; 2195 border: 1px solid rgba(217, 137, 46, 0.15); 2196 } 2197 2198 .native-instructions h4 { 2199 margin: 0 0 0.75rem 0; 2200 font-size: 0.875rem; 2201 color: var(--color-phosphor); 2202 } 2203 2204 .native-instructions ol { 2205 margin: 0; 2206 padding-left: 1.25rem; 2207 font-size: 0.875rem; 2208 color: var(--color-text-secondary); 2209 } 2210 2211 .native-instructions li { 2212 margin-bottom: 0.5rem; 2213 line-height: 1.5; 2214 } 2215 2216 .native-instructions code { 2217 background: rgba(217, 137, 46, 0.1); 2218 padding: 0.125rem 0.375rem; 2219 border-radius: 3px; 2220 font-family: var(--font-mono); 2221 font-size: 0.8125rem; 2222 color: var(--color-phosphor); 2223 } 2224 2225 .setup-note { 2226 margin-top: 0.75rem; 2227 padding: 0.75rem; 2228 background: rgba(217, 137, 46, 0.1); 2229 border-radius: 4px; 2230 font-size: 0.8125rem; 2231 line-height: 1.5; 2232 color: var(--color-text-secondary); 2233 } 2234 2235 .status-testing { 2236 background-color: rgba(59, 130, 246, 0.15); 2237 color: #60a5fa; 2238 } 2239 2240 .logseq-instructions { 2241 margin-top: 1rem; 2242 padding: 1rem; 2243 background: rgba(217, 137, 46, 0.05); 2244 border-radius: 6px; 2245 border: 1px solid rgba(217, 137, 46, 0.15); 2246 } 2247 2248 .logseq-instructions h4 { 2249 margin: 0 0 0.75rem 0; 2250 font-size: 0.875rem; 2251 color: var(--color-phosphor); 2252 } 2253 2254 .logseq-instructions ol { 2255 margin: 0; 2256 padding-left: 1.25rem; 2257 } 2258 2259 .logseq-instructions li { 2260 font-size: 0.8125rem; 2261 color: var(--color-text-muted); 2262 margin-bottom: 0.5rem; 2263 } 2264 2265 .logseq-instructions li:last-child { 2266 margin-bottom: 0; 2267 } 2268 2269 /* Todoist Integration */ 2270 .todoist-info { 2271 margin-bottom: 0.75rem; 2272 padding: 0.75rem; 2273 background: rgba(0, 0, 0, 0.2); 2274 border-radius: 4px; 2275 } 2276 2277 .todoist-instructions { 2278 margin-top: 1rem; 2279 padding: 1rem; 2280 background: rgba(217, 137, 46, 0.05); 2281 border-radius: 6px; 2282 border: 1px solid rgba(217, 137, 46, 0.15); 2283 } 2284 2285 .todoist-instructions h4 { 2286 margin: 0 0 0.75rem 0; 2287 font-size: 0.875rem; 2288 color: var(--color-phosphor); 2289 } 2290 2291 .todoist-instructions ol { 2292 margin: 0; 2293 padding-left: 1.25rem; 2294 } 2295 2296 .todoist-instructions li { 2297 font-size: 0.8125rem; 2298 color: var(--color-text-muted); 2299 margin-bottom: 0.5rem; 2300 } 2301 2302 .todoist-instructions li:last-child { 2303 margin-bottom: 0; 2304 } 2305 2306 /* Pause Media Sites */ 2307 .pause-media-section { 2308 grid-column: 1 / -1; 2309 } 2310 2311 .pause-media-status { 2312 display: flex; 2313 align-items: center; 2314 gap: 0.5rem; 2315 } 2316 2317 .site-count { 2318 padding: 0.25rem 0.5rem; 2319 background: rgba(46, 160, 67, 0.15); 2320 color: #2ea043; 2321 border-radius: 4px; 2322 font-size: 0.75rem; 2323 font-weight: 500; 2324 } 2325 2326 .site-count.custom { 2327 background: rgba(217, 137, 46, 0.15); 2328 color: var(--color-phosphor); 2329 } 2330 2331 .divider { 2332 color: var(--color-text-muted); 2333 margin: 0 0.25rem; 2334 } 2335 2336 .pause-media-preview { 2337 padding: 1rem; 2338 background: rgba(0, 0, 0, 0.2); 2339 border-radius: 6px; 2340 border: 1px solid rgba(217, 137, 46, 0.15); 2341 } 2342 2343 .sites-list { 2344 font-size: 0.875rem; 2345 margin-bottom: 0.75rem; 2346 } 2347 2348 .sites-list:last-child { 2349 margin-bottom: 0; 2350 } 2351 2352 .sites-list strong { 2353 color: var(--color-text-primary); 2354 display: block; 2355 margin-bottom: 0.25rem; 2356 } 2357 2358 .sites-preview { 2359 color: var(--color-text-muted); 2360 word-break: break-word; 2361 } 2362 2363 .custom-list { 2364 margin-top: 0.75rem; 2365 padding-top: 0.75rem; 2366 border-top: 1px solid rgba(217, 137, 46, 0.15); 2367 } 2368 2369 .pause-media-actions { 2370 display: flex; 2371 gap: 0.75rem; 2372 align-items: center; 2373 flex-wrap: wrap; 2374 } 2375 2376 .sites-textarea { 2377 width: 100%; 2378 padding: 0.75rem; 2379 background-color: var(--color-bg-dark); 2380 color: var(--color-text-primary); 2381 border: 1px solid rgba(217, 137, 46, 0.3); 2382 border-radius: 6px; 2383 font-family: var(--font-mono); 2384 font-size: 0.8125rem; 2385 line-height: 1.5; 2386 resize: vertical; 2387 min-height: 300px; 2388 } 2389 2390 .sites-textarea:focus { 2391 outline: none; 2392 border-color: var(--color-phosphor); 2393 } 2394 2395 .pause-media-editor .setting-description { 2396 margin: 0.5rem 0 1rem 0; 2397 } 2398 2399 .pause-media-editor .setting-description code { 2400 background: rgba(217, 137, 46, 0.1); 2401 padding: 0.125rem 0.375rem; 2402 border-radius: 3px; 2403 font-family: var(--font-mono); 2404 font-size: 0.8125rem; 2405 color: var(--color-phosphor); 2406 } 2407 2408 .pause-media-info { 2409 margin-top: 1rem; 2410 } 2411 2412 .pause-media-info .note { 2413 margin: 0; 2414 padding: 0.75rem; 2415 background: rgba(217, 137, 46, 0.1); 2416 border-radius: 4px; 2417 font-size: 0.8125rem; 2418 line-height: 1.5; 2419 } 2420 2421 .pause-media-info .note code { 2422 background: rgba(0, 0, 0, 0.2); 2423 padding: 0.125rem 0.375rem; 2424 border-radius: 3px; 2425 font-family: var(--font-mono); 2426 } 2427 2428 .btn-danger-outline { 2429 padding: 0.5rem 1rem; 2430 background: transparent; 2431 color: #ef4444; 2432 border: 1px solid rgba(239, 68, 68, 0.4); 2433 border-radius: 4px; 2434 font-weight: 500; 2435 cursor: pointer; 2436 transition: all 0.2s; 2437 } 2438 2439 .btn-danger-outline:hover { 2440 background: rgba(239, 68, 68, 0.1); 2441 border-color: rgba(239, 68, 68, 0.6); 2442 } 2443 2444 .save-indicator { 2445 color: #2ea043; 2446 font-size: 0.875rem; 2447 font-weight: 500; 2448 } 2449 2450 /* Graph Access for Attachments */ 2451 .graph-access-section { 2452 margin-top: 1.25rem; 2453 padding-top: 1.25rem; 2454 border-top: 1px solid rgba(217, 137, 46, 0.15); 2455 } 2456 2457 .subsection-title { 2458 font-size: 0.9375rem; 2459 color: var(--color-text-primary); 2460 margin: 0 0 0.5rem 0; 2461 font-weight: 500; 2462 } 2463 2464 .graph-access-status { 2465 margin: 0.75rem 0; 2466 padding: 0.75rem; 2467 background: rgba(0, 0, 0, 0.15); 2468 border-radius: 6px; 2469 } 2470 2471 .graph-directory-info { 2472 margin-top: 0.5rem; 2473 font-size: 0.875rem; 2474 } 2475 2476 .directory-label { 2477 color: var(--color-text-secondary); 2478 margin-right: 0.5rem; 2479 } 2480 2481 .directory-path { 2482 color: var(--color-phosphor); 2483 font-family: var(--font-mono); 2484 } 2485 2486 .setting-description code { 2487 background: rgba(0, 0, 0, 0.2); 2488 padding: 0.125rem 0.375rem; 2489 border-radius: 3px; 2490 font-family: var(--font-mono); 2491 font-size: 0.8125rem; 2492 } 2493 2494 .warning-text { 2495 color: var(--color-status-warning); 2496 } 2497 2498 .btn-secondary.btn-danger { 2499 color: var(--color-status-error); 2500 border-color: var(--color-status-error); 2501 } 2502 2503 .btn-secondary.btn-danger:hover:not(:disabled) { 2504 background-color: rgba(218, 54, 51, 0.1); 2505 } 2506 2507 /* Security / Credential Vault Styles */ 2508 .vault-setup-prompt { 2509 display: flex; 2510 align-items: center; 2511 gap: 1rem; 2512 padding: 1rem; 2513 background: rgba(217, 137, 46, 0.05); 2514 border: 1px solid rgba(217, 137, 46, 0.2); 2515 border-radius: 8px; 2516 } 2517 2518 .vault-prompt-text { 2519 flex: 1; 2520 } 2521 2522 .vault-prompt-text strong { 2523 color: var(--color-text-primary); 2524 } 2525 2526 .vault-prompt-text p { 2527 margin: 0.25rem 0 0; 2528 font-size: 0.875rem; 2529 color: var(--color-text-muted); 2530 } 2531 2532 .shield-icon { 2533 width: 40px; 2534 height: 40px; 2535 color: var(--color-phosphor); 2536 flex-shrink: 0; 2537 } 2538 2539 .shield-icon svg { 2540 width: 100%; 2541 height: 100%; 2542 } 2543 2544 .vault-setup-form { 2545 padding: 1rem; 2546 background: rgba(0, 0, 0, 0.15); 2547 border-radius: 8px; 2548 } 2549 2550 .vault-setup-form h4 { 2551 margin: 0 0 1rem; 2552 color: var(--color-text-primary); 2553 } 2554 2555 .vault-setup-form .input-group { 2556 margin-bottom: 1rem; 2557 } 2558 2559 .vault-setup-form .input-group label { 2560 display: block; 2561 margin-bottom: 0.375rem; 2562 font-size: 0.875rem; 2563 color: var(--color-text-secondary); 2564 } 2565 2566 .vault-setup-form .button-row { 2567 display: flex; 2568 justify-content: flex-end; 2569 gap: 0.75rem; 2570 margin-top: 1rem; 2571 } 2572 2573 .vault-status { 2574 padding: 1rem; 2575 background: rgba(46, 160, 67, 0.05); 2576 border: 1px solid rgba(46, 160, 67, 0.2); 2577 border-radius: 8px; 2578 } 2579 2580 .vault-status-header { 2581 display: flex; 2582 align-items: center; 2583 gap: 1rem; 2584 margin-bottom: 1rem; 2585 } 2586 2587 .vault-icon { 2588 width: 36px; 2589 height: 36px; 2590 flex-shrink: 0; 2591 } 2592 2593 .vault-icon svg { 2594 width: 100%; 2595 height: 100%; 2596 } 2597 2598 .vault-icon.unlocked { 2599 color: #2ea043; 2600 } 2601 2602 .vault-icon.locked { 2603 color: var(--color-phosphor); 2604 } 2605 2606 .vault-status-text strong { 2607 color: var(--color-text-primary); 2608 } 2609 2610 .vault-status-text p { 2611 margin: 0.25rem 0 0; 2612 font-size: 0.875rem; 2613 color: var(--color-text-muted); 2614 } 2615 2616 .vault-credentials-list { 2617 display: flex; 2618 gap: 0.75rem; 2619 flex-wrap: wrap; 2620 margin-bottom: 1rem; 2621 padding: 0.75rem; 2622 background: rgba(0, 0, 0, 0.15); 2623 border-radius: 6px; 2624 } 2625 2626 .cred-item { 2627 font-size: 0.8125rem; 2628 padding: 0.25rem 0.5rem; 2629 border-radius: 4px; 2630 } 2631 2632 .cred-item.stored { 2633 background: rgba(46, 160, 67, 0.15); 2634 color: #2ea043; 2635 } 2636 2637 .cred-item.empty { 2638 background: rgba(100, 100, 100, 0.15); 2639 color: var(--color-text-muted); 2640 } 2641 2642 .vault-actions { 2643 display: flex; 2644 gap: 0.75rem; 2645 } 2646 2647 .vault-locked { 2648 padding: 1rem; 2649 background: rgba(217, 137, 46, 0.05); 2650 border: 1px solid rgba(217, 137, 46, 0.2); 2651 border-radius: 8px; 2652 } 2653 2654 .vault-locked .vault-icon { 2655 margin-bottom: 0.75rem; 2656 } 2657 2658 .vault-locked-text { 2659 margin-bottom: 1rem; 2660 } 2661 2662 .vault-locked-text strong { 2663 color: var(--color-text-primary); 2664 } 2665 2666 .vault-locked-text p { 2667 margin: 0.25rem 0 0; 2668 font-size: 0.875rem; 2669 color: var(--color-text-muted); 2670 } 2671 2672 .vault-locked .input-group { 2673 display: flex; 2674 gap: 0.75rem; 2675 } 2676 2677 .vault-locked .input-group .text-input { 2678 flex: 1; 2679 } 2680 2681 .warning-box { 2682 display: flex; 2683 align-items: flex-start; 2684 gap: 0.75rem; 2685 padding: 0.75rem; 2686 background: rgba(245, 158, 11, 0.1); 2687 border: 1px solid rgba(245, 158, 11, 0.3); 2688 border-radius: 6px; 2689 font-size: 0.8125rem; 2690 color: var(--color-text-secondary); 2691 } 2692 2693 .warning-box svg { 2694 width: 1.25rem; 2695 height: 1.25rem; 2696 color: #f59e0b; 2697 flex-shrink: 0; 2698 margin-top: 0.125rem; 2699 } 2700 2701 .success-message { 2702 display: flex; 2703 align-items: center; 2704 gap: 0.5rem; 2705 padding: 0.75rem; 2706 background: rgba(46, 160, 67, 0.1); 2707 border-radius: 6px; 2708 color: #2ea043; 2709 font-size: 0.875rem; 2710 margin-bottom: 1rem; 2711 } 2712 2713 .success-message svg { 2714 width: 1rem; 2715 height: 1rem; 2716 flex-shrink: 0; 2717 } 2718 2719 .error-message { 2720 display: flex; 2721 align-items: center; 2722 gap: 0.5rem; 2723 padding: 0.75rem; 2724 background: rgba(239, 68, 68, 0.1); 2725 border-radius: 6px; 2726 color: #ef4444; 2727 font-size: 0.875rem; 2728 margin-bottom: 1rem; 2729 } 2730 2731 .error-message svg { 2732 width: 1rem; 2733 height: 1rem; 2734 flex-shrink: 0; 2735 } 2736 2737 .security-info { 2738 margin-top: 1rem; 2739 padding: 1rem; 2740 background: rgba(217, 137, 46, 0.05); 2741 border-radius: 6px; 2742 border: 1px solid rgba(217, 137, 46, 0.15); 2743 } 2744 2745 .security-info h4 { 2746 margin: 0 0 0.75rem 0; 2747 font-size: 0.875rem; 2748 color: var(--color-phosphor); 2749 } 2750 2751 .security-info ul { 2752 margin: 0; 2753 padding-left: 1.25rem; 2754 } 2755 2756 .security-info li { 2757 font-size: 0.8125rem; 2758 color: var(--color-text-muted); 2759 margin-bottom: 0.375rem; 2760 } 2761 2762 .security-info li:last-child { 2763 margin-bottom: 0; 2764 } 2765 2766 .button-row { 2767 display: flex; 2768 justify-content: flex-end; 2769 gap: 0.75rem; 2770 margin-top: 1rem; 2771 } 2772 2773 /* Responsive: stack on smaller screens */ 2774 @media (max-width: 768px) { 2775 .options { 2776 padding: 1.5rem; 2777 } 2778 2779 .options-content { 2780 grid-template-columns: 1fr; 2781 } 2782 2783 .options-content .settings-section:nth-last-child(-n+2) { 2784 grid-column: auto; 2785 } 2786 2787 .options-footer { 2788 justify-content: stretch; 2789 } 2790 2791 .btn-primary { 2792 width: 100%; 2793 } 2794 } 2795 </style>