702 lines
24 KiB
TypeScript
702 lines
24 KiB
TypeScript
/**
|
|
* <import-export-dialog> — Modal dialog for importing/exporting notes.
|
|
*
|
|
* Supports 4 sources: Logseq, Obsidian, Notion, Google Docs.
|
|
* File-based (Logseq/Obsidian) use ZIP upload/download.
|
|
* API-based (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' = 'import';
|
|
private activeSource: 'obsidian' | 'logseq' | 'notion' | 'google-docs' = 'obsidian';
|
|
private notebooks: NotebookOption[] = [];
|
|
private connections: ConnectionStatus = {
|
|
notion: { connected: false },
|
|
google: { connected: false },
|
|
logseq: { connected: true },
|
|
obsidian: { connected: true },
|
|
};
|
|
private remotePages: RemotePage[] = [];
|
|
private selectedPages = new Set<string>();
|
|
private importing = false;
|
|
private exporting = false;
|
|
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' = 'import') {
|
|
this.notebooks = notebooks;
|
|
this.activeTab = tab;
|
|
this.statusMessage = '';
|
|
this.selectedPages.clear();
|
|
this.selectedFile = null;
|
|
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 === 'obsidian' || this.activeSource === 'logseq') {
|
|
if (!this.selectedFile) {
|
|
this.setStatus('Please select a ZIP 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 sourceConnKey = this.activeSource === 'google-docs' ? 'google' : this.activeSource;
|
|
const isConnected = (this.connections as any)[sourceConnKey]?.connected || false;
|
|
|
|
this.shadow.innerHTML = `
|
|
<style>${this.getStyles()}</style>
|
|
<div class="dialog-overlay">
|
|
<div class="dialog">
|
|
<div class="dialog-header">
|
|
<h2>Import / Export</h2>
|
|
<button class="dialog-close" id="btn-close">×</button>
|
|
</div>
|
|
|
|
<div class="tab-bar">
|
|
<button class="tab ${this.activeTab === 'import' ? 'active' : ''}" data-tab="import">Import</button>
|
|
<button class="tab ${this.activeTab === 'export' ? 'active' : ''}" data-tab="export">Export</button>
|
|
</div>
|
|
|
|
<div class="source-bar">
|
|
${(['obsidian', 'logseq', 'notion', 'google-docs'] as const).map(s => `
|
|
<button class="source-btn ${this.activeSource === s ? 'active' : ''}" data-source="${s}">
|
|
${this.sourceIcon(s)} ${this.sourceName(s)}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<div class="dialog-body">
|
|
${this.activeTab === 'import' ? this.renderImportTab(isApiSource, isConnected) : this.renderExportTab(isApiSource, isConnected)}
|
|
|
|
<div class="status-message ${this.statusMessage ? `status-${this.statusType}` : ''}" style="display:${this.statusMessage ? 'block' : 'none'}">
|
|
${this.esc(this.statusMessage)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
this.attachListeners();
|
|
}
|
|
|
|
private renderImportTab(isApiSource: boolean, isConnected: boolean): string {
|
|
if (isApiSource && !isConnected) {
|
|
return `
|
|
<div class="connect-prompt">
|
|
<p>Connect your ${this.sourceName(this.activeSource)} account to import notes.</p>
|
|
<button class="btn-primary" id="btn-connect">Connect ${this.sourceName(this.activeSource)}</button>
|
|
</div>`;
|
|
}
|
|
|
|
if (isApiSource) {
|
|
// Show page list for selection
|
|
return `
|
|
<div class="page-list-header">
|
|
<span>Select pages to import:</span>
|
|
<button class="btn-secondary btn-sm" id="btn-refresh-pages">Refresh</button>
|
|
</div>
|
|
<div class="page-list">
|
|
${this.remotePages.length === 0
|
|
? '<div class="empty-list">No pages found. Click Refresh to load.</div>'
|
|
: this.remotePages.map(p => `
|
|
<label class="page-item">
|
|
<input type="checkbox" value="${p.id}" ${this.selectedPages.has(p.id) ? 'checked' : ''}>
|
|
<span class="page-icon">${p.icon || (this.activeSource === 'notion' ? 'N' : 'G')}</span>
|
|
<span class="page-title">${this.esc(p.title)}</span>
|
|
</label>
|
|
`).join('')}
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Target notebook:</label>
|
|
<select id="target-notebook">
|
|
<option value="">Create new notebook</option>
|
|
${this.notebooks.map(nb => `<option value="${nb.id}">${this.esc(nb.title)}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<button class="btn-primary" id="btn-import" ${this.importing ? 'disabled' : ''}>
|
|
${this.importing ? 'Importing...' : `Import Selected (${this.selectedPages.size})`}
|
|
</button>`;
|
|
}
|
|
|
|
// File-based import (Obsidian/Logseq)
|
|
return `
|
|
<div class="upload-area" id="upload-area">
|
|
<p>Upload a ZIP of your ${this.sourceName(this.activeSource)} vault</p>
|
|
<input type="file" id="file-input" accept=".zip" style="display:none">
|
|
<button class="btn-secondary" id="btn-choose-file">
|
|
${this.selectedFile ? this.esc(this.selectedFile.name) : 'Choose File'}
|
|
</button>
|
|
<p class="upload-hint">or drag & drop a ZIP file here</p>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Target notebook:</label>
|
|
<select id="target-notebook">
|
|
<option value="">Create new notebook</option>
|
|
${this.notebooks.map(nb => `<option value="${nb.id}">${this.esc(nb.title)}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<button class="btn-primary" id="btn-import" ${this.importing || !this.selectedFile ? 'disabled' : ''}>
|
|
${this.importing ? 'Importing...' : 'Import'}
|
|
</button>`;
|
|
}
|
|
|
|
private renderExportTab(isApiSource: boolean, isConnected: boolean): string {
|
|
const formats = this.activeSource === 'notion' || this.activeSource === 'google-docs'
|
|
? '' : '';
|
|
|
|
if (isApiSource && !isConnected) {
|
|
return `
|
|
<div class="connect-prompt">
|
|
<p>Connect your ${this.sourceName(this.activeSource)} account to export notes.</p>
|
|
<button class="btn-primary" id="btn-connect">Connect ${this.sourceName(this.activeSource)}</button>
|
|
</div>`;
|
|
}
|
|
|
|
return `
|
|
<div class="form-row">
|
|
<label>Source notebook:</label>
|
|
<select id="target-notebook">
|
|
<option value="">Select a notebook</option>
|
|
${this.notebooks.map(nb => `<option value="${nb.id}">${this.esc(nb.title)}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<button class="btn-primary" id="btn-export" ${this.exporting ? 'disabled' : ''}>
|
|
${this.exporting ? 'Exporting...'
|
|
: isApiSource ? `Export to ${this.sourceName(this.activeSource)}`
|
|
: 'Download ZIP'}
|
|
</button>`;
|
|
}
|
|
|
|
private sourceName(s: string): string {
|
|
const names: Record<string, string> = {
|
|
obsidian: 'Obsidian',
|
|
logseq: 'Logseq',
|
|
notion: 'Notion',
|
|
'google-docs': 'Google Docs',
|
|
};
|
|
return names[s] || s;
|
|
}
|
|
|
|
private sourceIcon(s: string): string {
|
|
const icons: Record<string, string> = {
|
|
obsidian: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l6 3.5v7L8 15l-6-3.5v-7L8 1z"/></svg>',
|
|
logseq: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="6"/></svg>',
|
|
notion: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><rect x="2" y="2" width="12" height="12" rx="2"/></svg>',
|
|
'google-docs': '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 1h6l4 4v10H4V1z"/></svg>',
|
|
};
|
|
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;
|
|
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.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', () => {
|
|
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 file = (e as DragEvent).dataTransfer?.files[0];
|
|
if (file && file.name.endsWith('.zip')) {
|
|
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());
|
|
}
|
|
|
|
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); 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;
|
|
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); }
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('import-export-dialog', ImportExportDialog);
|
|
|
|
export { ImportExportDialog };
|