/** * — Modal dialog for importing, exporting, and syncing notes. * * Import sources: Files (direct), Obsidian, Logseq, Notion, Google Docs, Evernote, Roam Research. * Export targets: Obsidian, Logseq, Notion, Google Docs. * Sync: Re-fetch from API sources (Notion/Google) or re-upload vault ZIPs for file-based. * * File-based sources (Obsidian/Logseq/Evernote/Roam) use file upload. * API-based sources (Notion/Google) use OAuth connections. */ interface NotebookOption { id: string; title: string; } interface ConnectionStatus { notion: { connected: boolean; workspaceName?: string }; google: { connected: boolean; email?: string }; logseq: { connected: boolean }; obsidian: { connected: boolean }; } interface RemotePage { id: string; title: string; lastEdited?: string; lastModified?: string; icon?: string; } class ImportExportDialog extends HTMLElement { private shadow!: ShadowRoot; private space = ''; private activeTab: 'import' | 'export' | 'sync' = 'import'; private activeSource: 'files' | 'obsidian' | 'logseq' | 'notion' | 'google-docs' | 'evernote' | 'roam' = 'files'; private notebooks: NotebookOption[] = []; private connections: ConnectionStatus & { evernote?: { connected: boolean }; roam?: { connected: boolean } } = { notion: { connected: false }, google: { connected: false }, logseq: { connected: true }, obsidian: { connected: true }, }; private selectedFiles: File[] = []; private remotePages: RemotePage[] = []; private selectedPages = new Set(); private importing = false; private exporting = false; private syncing = false; private syncStatuses: Record = {}; private statusMessage = ''; private statusType: 'info' | 'success' | 'error' = 'info'; private selectedFile: File | null = null; private targetNotebookId = ''; constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); } connectedCallback() { this.space = this.getAttribute('space') || 'demo'; this.render(); this.loadConnections(); } /** Open the dialog. */ open(notebooks: NotebookOption[], tab: 'import' | 'export' | 'sync' = 'import') { this.notebooks = notebooks; this.activeTab = tab; this.statusMessage = ''; this.selectedPages.clear(); this.selectedFile = null; this.selectedFiles = []; this.syncStatuses = {}; this.render(); (this.shadow.querySelector('.dialog-overlay') as HTMLElement)?.classList.add('open'); } /** Close the dialog. */ close() { (this.shadow.querySelector('.dialog-overlay') as HTMLElement)?.classList.remove('open'); } private async loadConnections() { try { const res = await fetch(`/${this.space}/rnotes/api/connections`); if (res.ok) { this.connections = await res.json(); } } catch { /* ignore */ } } private async loadRemotePages() { this.remotePages = []; this.selectedPages.clear(); if (this.activeSource === 'notion') { try { const res = await fetch(`/${this.space}/rnotes/api/import/notion/pages`); if (res.ok) { const data = await res.json(); this.remotePages = data.pages || []; } } catch { /* ignore */ } } else if (this.activeSource === 'google-docs') { try { const res = await fetch(`/${this.space}/rnotes/api/import/google-docs/list`); if (res.ok) { const data = await res.json(); this.remotePages = data.docs || []; } } catch { /* ignore */ } } this.render(); (this.shadow.querySelector('.dialog-overlay') as HTMLElement)?.classList.add('open'); } private setStatus(msg: string, type: 'info' | 'success' | 'error' = 'info') { this.statusMessage = msg; this.statusType = type; const statusEl = this.shadow.querySelector('.status-message') as HTMLElement; if (statusEl) { statusEl.textContent = msg; statusEl.className = `status-message status-${type}`; statusEl.style.display = msg ? 'block' : 'none'; } } private async handleImport() { this.importing = true; this.setStatus('Importing...', 'info'); try { if (this.activeSource === 'files') { if (this.selectedFiles.length === 0) { this.setStatus('Please select at least one file', 'error'); this.importing = false; return; } const formData = new FormData(); for (const f of this.selectedFiles) formData.append('files', f); if (this.targetNotebookId) formData.append('notebookId', this.targetNotebookId); const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/import/files`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); const data = await res.json(); if (data.ok) { this.setStatus(`Imported ${data.imported} note${data.imported !== 1 ? 's' : ''}${data.warnings?.length ? ` (${data.warnings.length} warnings)` : ''}`, 'success'); this.dispatchEvent(new CustomEvent('import-complete', { detail: data })); } else { this.setStatus(data.error || 'Import failed', 'error'); } } else if (this.activeSource === 'obsidian' || this.activeSource === 'logseq' || this.activeSource === 'evernote' || this.activeSource === 'roam') { if (!this.selectedFile) { const fileTypeHint: Record = { obsidian: 'ZIP file', logseq: 'ZIP file', evernote: '.enex file', roam: 'JSON file', }; this.setStatus(`Please select a ${fileTypeHint[this.activeSource] || 'file'}`, 'error'); this.importing = false; return; } const formData = new FormData(); formData.append('file', this.selectedFile); formData.append('source', this.activeSource); if (this.targetNotebookId) { formData.append('notebookId', this.targetNotebookId); } const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/import/upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); const data = await res.json(); if (data.ok) { this.setStatus( `Imported ${data.imported} notes${data.updated ? `, updated ${data.updated}` : ''}${data.warnings?.length ? ` (${data.warnings.length} warnings)` : ''}`, 'success' ); this.dispatchEvent(new CustomEvent('import-complete', { detail: data })); } else { this.setStatus(data.error || 'Import failed', 'error'); } } else if (this.activeSource === 'notion') { if (this.selectedPages.size === 0) { this.setStatus('Please select at least one page', 'error'); this.importing = false; return; } const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/import/notion`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ pageIds: Array.from(this.selectedPages), notebookId: this.targetNotebookId || undefined, }), }); const data = await res.json(); if (data.ok) { this.setStatus(`Imported ${data.imported} notes from Notion`, 'success'); this.dispatchEvent(new CustomEvent('import-complete', { detail: data })); } else { this.setStatus(data.error || 'Notion import failed', 'error'); } } else if (this.activeSource === 'google-docs') { if (this.selectedPages.size === 0) { this.setStatus('Please select at least one document', 'error'); this.importing = false; return; } const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/import/google-docs`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ docIds: Array.from(this.selectedPages), notebookId: this.targetNotebookId || undefined, }), }); const data = await res.json(); if (data.ok) { this.setStatus(`Imported ${data.imported} notes from Google Docs`, 'success'); this.dispatchEvent(new CustomEvent('import-complete', { detail: data })); } else { this.setStatus(data.error || 'Google Docs import failed', 'error'); } } } catch (err) { this.setStatus(`Import error: ${(err as Error).message}`, 'error'); } this.importing = false; } private async handleExport() { if (!this.targetNotebookId) { this.setStatus('Please select a notebook to export', 'error'); return; } this.exporting = true; this.setStatus('Exporting...', 'info'); try { if (this.activeSource === 'obsidian' || this.activeSource === 'logseq' || this.activeSource === 'markdown' as any) { const format = this.activeSource === 'markdown' as any ? 'markdown' : this.activeSource; const url = `/${this.space}/rnotes/api/export/${format}?notebookId=${encodeURIComponent(this.targetNotebookId)}`; const res = await fetch(url); if (res.ok) { const blob = await res.blob(); const disposition = res.headers.get('Content-Disposition') || ''; const filename = disposition.match(/filename="(.+)"/)?.[1] || `export-${format}.zip`; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); this.setStatus('Download started!', 'success'); } else { const data = await res.json(); this.setStatus(data.error || 'Export failed', 'error'); } } else if (this.activeSource === 'notion') { const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/export/notion`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ notebookId: this.targetNotebookId }), }); const data = await res.json(); if (data.exported) { this.setStatus(`Exported ${data.exported.length} notes to Notion`, 'success'); } else { this.setStatus(data.error || 'Notion export failed', 'error'); } } else if (this.activeSource === 'google-docs') { const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/export/google-docs`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ notebookId: this.targetNotebookId }), }); const data = await res.json(); if (data.exported) { this.setStatus(`Exported ${data.exported.length} notes to Google Docs`, 'success'); } else { this.setStatus(data.error || 'Google Docs export failed', 'error'); } } } catch (err) { this.setStatus(`Export error: ${(err as Error).message}`, 'error'); } this.exporting = false; } private esc(s: string): string { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } private render() { const isApiSource = this.activeSource === 'notion' || this.activeSource === 'google-docs'; const isFileSource = this.activeSource === 'files'; // File-based sources (files, obsidian, logseq, evernote, roam) are always "connected" — no auth needed const fileBased = ['files', 'obsidian', 'logseq', 'evernote', 'roam']; const sourceConnKey = this.activeSource === 'google-docs' ? 'google' : this.activeSource; const isConnected = fileBased.includes(this.activeSource) || (this.connections as any)[sourceConnKey]?.connected || false; this.shadow.innerHTML = `

${this.activeTab === 'sync' ? 'Sync' : this.activeTab === 'export' ? 'Export' : 'Import'} Notes

${this.activeTab !== 'sync' ? (() => { const sources = this.activeTab === 'export' ? (['obsidian', 'logseq', 'notion', 'google-docs'] as const) : (['files', 'obsidian', 'logseq', 'notion', 'google-docs', 'evernote', 'roam'] as const); return `
${sources.map(s => ` `).join('')}
`; })() : ''}
${this.activeTab === 'sync' ? this.renderSyncTab() : this.activeTab === 'import' ? this.renderImportTab(isApiSource, isConnected) : this.renderExportTab(isApiSource, isConnected)}
${this.esc(this.statusMessage)}
`; this.attachListeners(); } private renderImportTab(isApiSource: boolean, isConnected: boolean): string { if (isApiSource && !isConnected) { return `

Connect your ${this.sourceName(this.activeSource)} account to import notes.

`; } if (isApiSource) { // Show page list for selection return `
Select pages to import:
${this.remotePages.length === 0 ? '
No pages found. Click Refresh to load.
' : this.remotePages.map(p => ` `).join('')}
`; } // Generic file import if (this.activeSource === 'files') { return `

Drop files to import as notes

.md .txt .html .jpg .png .webp — drag & drop supported

`; } // File-based source import (Obsidian/Logseq/Evernote/Roam) const acceptMap: Record = { obsidian: '.zip', logseq: '.zip', evernote: '.enex,.zip', roam: '.json,.zip', }; const hintMap: Record = { obsidian: 'Upload a ZIP of your Obsidian vault', logseq: 'Upload a ZIP of your Logseq graph', evernote: 'Upload an .enex export file', roam: 'Upload a Roam Research JSON export', }; return `

${hintMap[this.activeSource] || 'Upload a file'}

or drag & drop here

`; } private renderExportTab(isApiSource: boolean, isConnected: boolean): string { if (isApiSource && !isConnected) { return `

Connect your ${this.sourceName(this.activeSource)} account to export notes.

`; } return `
`; } private renderSyncTab(): string { const statusEntries = Object.entries(this.syncStatuses); const hasApiNotes = statusEntries.some(([_, s]) => s.source === 'notion' || s.source === 'google-docs'); const hasFileNotes = statusEntries.some(([_, s]) => s.source === 'obsidian' || s.source === 'logseq'); return `
${this.targetNotebookId ? `
${statusEntries.length === 0 ? '

No imported notes found in this notebook. Import notes first to enable sync.

' : `

${statusEntries.length} synced note${statusEntries.length !== 1 ? 's' : ''} found

${statusEntries.map(([id, s]) => `
${s.source} ${s.hasConflict ? 'conflict' : s.syncStatus || 'synced'} ${s.lastSyncedAt ? `${this.relativeTime(s.lastSyncedAt)}` : ''}
`).join('')}
` }
${hasApiNotes ? ` ` : ''} ${hasFileNotes ? `

File-based sources require re-uploading your vault ZIP:

` : ''} ` : ''} `; } private relativeTime(ts: number): string { const diff = Date.now() - ts; if (diff < 60000) return 'just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return `${Math.floor(diff / 86400000)}d ago`; } private async loadSyncStatus(notebookId: string) { try { const res = await fetch(`/${this.space}/rnotes/api/sync/status/${notebookId}`); if (res.ok) { const data = await res.json(); this.syncStatuses = data.statuses || {}; } } catch { /* ignore */ } this.render(); (this.shadow.querySelector('.dialog-overlay') as HTMLElement)?.classList.add('open'); } private async handleSyncApi() { if (!this.targetNotebookId) return; this.syncing = true; this.setStatus('Syncing...', 'info'); try { const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/sync/notebook/${this.targetNotebookId}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, }); const data = await res.json(); if (data.ok) { const parts: string[] = []; if (data.synced > 0) parts.push(`${data.synced} updated`); if (data.conflicts > 0) parts.push(`${data.conflicts} conflicts`); if (data.errors > 0) parts.push(`${data.errors} errors`); this.setStatus(parts.length > 0 ? `Sync complete: ${parts.join(', ')}` : 'All notes up to date', 'success'); this.dispatchEvent(new CustomEvent('sync-complete', { detail: data })); this.loadSyncStatus(this.targetNotebookId); } else { this.setStatus(data.error || 'Sync failed', 'error'); } } catch (err) { this.setStatus(`Sync error: ${(err as Error).message}`, 'error'); } this.syncing = false; } private async handleSyncUpload(file: File) { if (!this.targetNotebookId) return; this.syncing = true; this.setStatus('Syncing from ZIP...', 'info'); // Detect source from sync statuses const sources = new Set(Object.values(this.syncStatuses).map(s => s.source)); const source = sources.has('obsidian') ? 'obsidian' : sources.has('logseq') ? 'logseq' : ''; if (!source) { this.setStatus('Could not determine source type', 'error'); this.syncing = false; return; } try { const formData = new FormData(); formData.append('file', file); formData.append('notebookId', this.targetNotebookId); formData.append('source', source); const token = localStorage.getItem('encryptid_token') || ''; const res = await fetch(`/${this.space}/rnotes/api/sync/upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); const data = await res.json(); if (data.ok) { this.setStatus(`Sync: ${data.synced} updated, ${data.conflicts} conflicts`, 'success'); this.dispatchEvent(new CustomEvent('sync-complete', { detail: data })); this.loadSyncStatus(this.targetNotebookId); } else { this.setStatus(data.error || 'Sync failed', 'error'); } } catch (err) { this.setStatus(`Sync error: ${(err as Error).message}`, 'error'); } this.syncing = false; } private sourceName(s: string): string { const names: Record = { files: 'Files', obsidian: 'Obsidian', logseq: 'Logseq', notion: 'Notion', 'google-docs': 'Google Docs', evernote: 'Evernote', roam: 'Roam', }; return names[s] || s; } private sourceIcon(s: string): string { const icons: Record = { files: '', obsidian: '', logseq: '', notion: '', 'google-docs': '', evernote: '', roam: '', }; return icons[s] || ''; } private attachListeners() { // Close button this.shadow.getElementById('btn-close')?.addEventListener('click', () => this.close()); // Overlay click to close this.shadow.querySelector('.dialog-overlay')?.addEventListener('click', (e) => { if ((e.target as HTMLElement).classList.contains('dialog-overlay')) this.close(); }); // Tab switching this.shadow.querySelectorAll('.tab').forEach(btn => { btn.addEventListener('click', () => { this.activeTab = (btn as HTMLElement).dataset.tab as any; // Auto-select valid source when switching to export (non-exportable: files, evernote, roam) if (this.activeTab === 'export' && ['files', 'evernote', 'roam'].includes(this.activeSource)) { this.activeSource = 'obsidian'; } this.statusMessage = ''; this.render(); (this.shadow.querySelector('.dialog-overlay') as HTMLElement)?.classList.add('open'); }); }); // Source switching this.shadow.querySelectorAll('.source-btn').forEach(btn => { btn.addEventListener('click', () => { this.activeSource = (btn as HTMLElement).dataset.source as any; this.remotePages = []; this.selectedPages.clear(); this.selectedFile = null; this.selectedFiles = []; this.statusMessage = ''; this.render(); (this.shadow.querySelector('.dialog-overlay') as HTMLElement)?.classList.add('open'); }); }); // File input const fileInput = this.shadow.getElementById('file-input') as HTMLInputElement; const chooseBtn = this.shadow.getElementById('btn-choose-file'); chooseBtn?.addEventListener('click', () => fileInput?.click()); fileInput?.addEventListener('change', () => { if (this.activeSource === 'files') { this.selectedFiles = Array.from(fileInput.files || []); if (chooseBtn) chooseBtn.textContent = this.selectedFiles.length > 0 ? `${this.selectedFiles.length} file${this.selectedFiles.length > 1 ? 's' : ''} selected` : 'Choose Files'; const importBtn = this.shadow.getElementById('btn-import') as HTMLButtonElement; if (importBtn) importBtn.disabled = this.selectedFiles.length === 0; } else { this.selectedFile = fileInput.files?.[0] || null; if (chooseBtn) chooseBtn.textContent = this.selectedFile?.name || 'Choose File'; const importBtn = this.shadow.getElementById('btn-import') as HTMLButtonElement; if (importBtn) importBtn.disabled = !this.selectedFile; } }); // Drag & drop const uploadArea = this.shadow.getElementById('upload-area'); if (uploadArea) { uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover')); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); const files = (e as DragEvent).dataTransfer?.files; if (!files || files.length === 0) return; if (this.activeSource === 'files') { this.selectedFiles = Array.from(files); if (chooseBtn) chooseBtn.textContent = `${this.selectedFiles.length} file${this.selectedFiles.length > 1 ? 's' : ''} selected`; const importBtn = this.shadow.getElementById('btn-import') as HTMLButtonElement; if (importBtn) importBtn.disabled = false; } else { const file = files[0]; this.selectedFile = file; if (chooseBtn) chooseBtn.textContent = file.name; const importBtn = this.shadow.getElementById('btn-import') as HTMLButtonElement; if (importBtn) importBtn.disabled = false; } }); } // Target notebook select const notebookSelect = this.shadow.getElementById('target-notebook') as HTMLSelectElement; notebookSelect?.addEventListener('change', () => { this.targetNotebookId = notebookSelect.value; }); // Page checkboxes this.shadow.querySelectorAll('.page-item input[type="checkbox"]').forEach(cb => { cb.addEventListener('change', () => { const input = cb as HTMLInputElement; if (input.checked) { this.selectedPages.add(input.value); } else { this.selectedPages.delete(input.value); } const importBtn = this.shadow.getElementById('btn-import'); if (importBtn) importBtn.textContent = `Import Selected (${this.selectedPages.size})`; }); }); // Refresh pages this.shadow.getElementById('btn-refresh-pages')?.addEventListener('click', () => { this.loadRemotePages(); }); // Connect button this.shadow.getElementById('btn-connect')?.addEventListener('click', () => { const provider = this.activeSource === 'google-docs' ? 'google' : this.activeSource; window.location.href = `/api/oauth/${provider}/authorize?space=${this.space}`; }); // Import button this.shadow.getElementById('btn-import')?.addEventListener('click', () => this.handleImport()); // Export button this.shadow.getElementById('btn-export')?.addEventListener('click', () => this.handleExport()); // Sync tab listeners const syncNotebookSelect = this.shadow.getElementById('sync-notebook') as HTMLSelectElement; syncNotebookSelect?.addEventListener('change', () => { this.targetNotebookId = syncNotebookSelect.value; if (this.targetNotebookId) { this.loadSyncStatus(this.targetNotebookId); } else { this.syncStatuses = {}; this.render(); (this.shadow.querySelector('.dialog-overlay') as HTMLElement)?.classList.add('open'); } }); this.shadow.getElementById('btn-sync-api')?.addEventListener('click', () => this.handleSyncApi()); const syncFileInput = this.shadow.getElementById('sync-file-input') as HTMLInputElement; this.shadow.getElementById('btn-sync-choose-file')?.addEventListener('click', () => syncFileInput?.click()); syncFileInput?.addEventListener('change', () => { const file = syncFileInput.files?.[0]; if (file) this.handleSyncUpload(file); }); } private getStyles(): string { return ` :host { display: block; } .dialog-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); z-index: 10000; justify-content: center; align-items: center; } .dialog-overlay.open { display: flex; } .dialog { background: var(--rs-bg-surface, #1a1a2e); border: 1px solid var(--rs-border, #2a2a4a); border-radius: 16px; width: 560px; max-width: 95vw; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 24px 80px rgba(0,0,0,0.5); } .dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--rs-border-subtle, #2a2a4a); } .dialog-header h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--rs-text-primary, #e0e0e0); } .dialog-close { background: none; border: none; color: var(--rs-text-muted, #888); font-size: 22px; cursor: pointer; padding: 0 4px; line-height: 1; } .dialog-close:hover { color: var(--rs-text-primary, #e0e0e0); } .tab-bar { display: flex; gap: 0; border-bottom: 1px solid var(--rs-border-subtle, #2a2a4a); } .tab { flex: 1; padding: 10px; border: none; background: none; color: var(--rs-text-secondary, #aaa); font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; border-bottom: 2px solid transparent; } .tab.active { color: var(--rs-primary, #6366f1); border-bottom-color: var(--rs-primary, #6366f1); } .tab:hover { color: var(--rs-text-primary, #e0e0e0); } .source-bar { display: flex; gap: 4px; padding: 12px 16px; flex-wrap: wrap; border-bottom: 1px solid var(--rs-border-subtle, #2a2a4a); } .source-btn { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--rs-border, #2a2a4a); background: transparent; color: var(--rs-text-secondary, #aaa); font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; transition: all 0.15s; } .source-btn:hover { border-color: var(--rs-border-strong, #444); color: var(--rs-text-primary, #e0e0e0); } .source-btn.active { background: var(--rs-primary, #6366f1); color: #fff; border-color: var(--rs-primary, #6366f1); } .dialog-body { padding: 16px 20px; overflow-y: auto; flex: 1; } .upload-area { border: 2px dashed var(--rs-border, #2a2a4a); border-radius: 10px; padding: 24px; text-align: center; margin-bottom: 16px; transition: border-color 0.15s, background 0.15s; } .upload-area.dragover { border-color: var(--rs-primary, #6366f1); background: rgba(99,102,241,0.05); } .upload-area p { margin: 0 0 8px; color: var(--rs-text-secondary, #aaa); font-size: 13px; } .upload-hint { font-size: 11px !important; color: var(--rs-text-muted, #666) !important; margin-top: 8px !important; } .form-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; } .form-row label { font-size: 13px; color: var(--rs-text-secondary, #aaa); white-space: nowrap; } .form-row select { flex: 1; padding: 7px 10px; border-radius: 6px; border: 1px solid var(--rs-border, #2a2a4a); background: var(--rs-input-bg, #111); color: var(--rs-text-primary, #e0e0e0); font-size: 13px; } .btn-primary { width: 100%; padding: 10px; border-radius: 8px; border: none; background: var(--rs-primary, #6366f1); color: #fff; font-weight: 600; font-size: 13px; cursor: pointer; transition: background 0.15s; } .btn-primary:hover { background: var(--rs-primary-hover, #5558e6); } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } .btn-secondary { padding: 8px 16px; border-radius: 6px; border: 1px solid var(--rs-border, #2a2a4a); background: transparent; color: var(--rs-text-primary, #e0e0e0); font-size: 13px; cursor: pointer; } .btn-secondary:hover { border-color: var(--rs-border-strong, #444); } .btn-sm { padding: 4px 10px; font-size: 11px; } .connect-prompt { text-align: center; padding: 24px; } .connect-prompt p { color: var(--rs-text-secondary, #aaa); margin-bottom: 16px; font-size: 13px; } .page-list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .page-list-header span { font-size: 13px; color: var(--rs-text-secondary, #aaa); } .page-list { max-height: 200px; overflow-y: auto; border: 1px solid var(--rs-border-subtle, #2a2a4a); border-radius: 8px; margin-bottom: 14px; } .page-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; transition: background 0.1s; border-bottom: 1px solid var(--rs-border-subtle, #1a1a2e); } .page-item:last-child { border-bottom: none; } .page-item:hover { background: rgba(255,255,255,0.03); } .page-item input[type="checkbox"] { accent-color: var(--rs-primary, #6366f1); } .page-icon { width: 20px; height: 20px; font-size: 12px; display: flex; align-items: center; justify-content: center; background: var(--rs-bg-surface-raised, #222); border-radius: 4px; color: var(--rs-text-muted, #888); } .page-title { font-size: 13px; color: var(--rs-text-primary, #e0e0e0); flex: 1; } .empty-list { padding: 20px; text-align: center; color: var(--rs-text-muted, #666); font-size: 12px; } .status-message { margin-top: 12px; padding: 8px 12px; border-radius: 6px; font-size: 12px; text-align: center; } .status-info { background: rgba(99,102,241,0.1); color: var(--rs-primary, #6366f1); } .status-success { background: rgba(34,197,94,0.1); color: var(--rs-success, #22c55e); } .status-error { background: rgba(239,68,68,0.1); color: var(--rs-error, #ef4444); } .sync-summary { margin: 12px 0; } .sync-empty { font-size: 12px; color: var(--rs-text-muted, #666); text-align: center; padding: 20px; } .sync-count { font-size: 12px; color: var(--rs-text-secondary, #aaa); margin: 0 0 8px; } .sync-list { max-height: 200px; overflow-y: auto; border: 1px solid var(--rs-border-subtle, #2a2a4a); border-radius: 8px; margin-bottom: 14px; } .sync-item { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-bottom: 1px solid var(--rs-border-subtle, #1a1a2e); font-size: 12px; } .sync-item:last-child { border-bottom: none; } .sync-source-badge { padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background: rgba(99,102,241,0.15); color: var(--rs-primary, #6366f1); } .sync-source-badge.notion { background: rgba(255,255,255,0.08); } .sync-source-badge.google-docs { background: rgba(66,133,244,0.15); color: #4285f4; } .sync-source-badge.obsidian { background: rgba(126,100,255,0.15); color: #7e64ff; } .sync-source-badge.logseq { background: rgba(133,211,127,0.15); color: #85d37f; } .sync-status { font-size: 10px; padding: 2px 6px; border-radius: 4px; } .sync-status.synced { background: rgba(34,197,94,0.1); color: var(--rs-success, #22c55e); } .sync-status.conflict { background: rgba(239,68,68,0.1); color: var(--rs-error, #ef4444); } .sync-status.local-modified { background: rgba(250,204,21,0.1); color: #facc15; } .sync-status.remote-modified { background: rgba(99,102,241,0.1); color: var(--rs-primary, #6366f1); } .sync-time { color: var(--rs-text-muted, #666); margin-left: auto; font-size: 10px; } .sync-upload { margin-top: 12px; } `; } } customElements.define('import-export-dialog', ImportExportDialog); export { ImportExportDialog };