/ src / components / bookmarks / ImportBookmarksModal.svelte
ImportBookmarksModal.svelte
  1  <script lang="ts">
  2    import type { BookmarkFolder, DetectedStructure, ImportMode } from '../../lib/bookmarks/types';
  3    import type { ParentWorkspace } from '../../lib/types';
  4    import { Modal, Button } from '../common';
  5  
  6    interface FolderWithMeta extends BookmarkFolder {
  7      structure: DetectedStructure;
  8      recommendedMode: ImportMode;
  9    }
 10  
 11    interface Props {
 12      open: boolean;
 13      parentWorkspaces: ParentWorkspace[];
 14      onclose: () => void;
 15      onImported: () => void;
 16    }
 17  
 18    let { open = $bindable(), parentWorkspaces, onclose, onImported }: Props = $props();
 19  
 20    // State
 21    let loading = $state(false);
 22    let error = $state<string | null>(null);
 23    let folders = $state<FolderWithMeta[]>([]);
 24    let selectedFolder = $state<FolderWithMeta | null>(null);
 25    let importMode = $state<ImportMode>('auto');
 26    let selectedParentId = $state<string>('');
 27    let importing = $state(false);
 28  
 29    // Load bookmark folders when modal opens
 30    $effect(() => {
 31      if (open) {
 32        loadFolders();
 33      } else {
 34        // Reset state when closed
 35        error = null;
 36        selectedFolder = null;
 37        importMode = 'auto';
 38        selectedParentId = '';
 39      }
 40    });
 41  
 42    async function loadFolders() {
 43      loading = true;
 44      error = null;
 45  
 46      try {
 47        const response = await browser.runtime.sendMessage({ type: 'GET_BOOKMARK_FOLDERS' });
 48        if (response.success) {
 49          folders = response.folders;
 50        } else {
 51          error = response.error || 'Failed to load bookmark folders';
 52        }
 53      } catch (e) {
 54        error = e instanceof Error ? e.message : 'Failed to load bookmarks';
 55      } finally {
 56        loading = false;
 57      }
 58    }
 59  
 60    function selectFolder(folder: FolderWithMeta) {
 61      selectedFolder = folder;
 62      importMode = 'auto';
 63      selectedParentId = '';
 64    }
 65  
 66    function getEffectiveMode(): 'standalone' | 'context' {
 67      if (importMode === 'auto') {
 68        return selectedFolder?.recommendedMode === 'context' ? 'context' : 'standalone';
 69      }
 70      return importMode;
 71    }
 72  
 73    async function handleImport() {
 74      if (!selectedFolder) return;
 75  
 76      importing = true;
 77      error = null;
 78  
 79      try {
 80        // Clone selectedFolder to avoid Firefox "Proxy object could not be cloned" error
 81        const folderData = JSON.parse(JSON.stringify(selectedFolder));
 82        const response = await browser.runtime.sendMessage({
 83          type: 'IMPORT_FROM_BOOKMARKS',
 84          payload: {
 85            folderId: selectedFolder.id,
 86            folderData,
 87            mode: importMode,
 88            parentId: selectedParentId || undefined
 89          }
 90        });
 91  
 92        if (response.success) {
 93          onImported();
 94          onclose();
 95        } else {
 96          error = response.error || 'Failed to import bookmarks';
 97        }
 98      } catch (e) {
 99        error = e instanceof Error ? e.message : 'Failed to import bookmarks';
100      } finally {
101        importing = false;
102      }
103    }
104  
105    function formatBookmarkCount(count: number): string {
106      return `${count} bookmark${count !== 1 ? 's' : ''}`;
107    }
108  </script>
109  
110  <Modal bind:open title="Import from Bookmarks" size="lg">
111    <div class="space-y-4">
112      {#if loading}
113        <div class="flex items-center justify-center py-8">
114          <div class="flex items-center gap-3 text-text-muted">
115            <svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
116              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
117              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
118            </svg>
119            <span>Loading bookmark folders...</span>
120          </div>
121        </div>
122      {:else if error}
123        <div class="bg-status-error/10 border border-status-error/30 rounded-lg p-4 text-sm text-status-error">
124          {error}
125        </div>
126      {:else if folders.length === 0}
127        <div class="text-center py-8 text-text-muted">
128          <svg class="w-12 h-12 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
129            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
130          </svg>
131          <p class="mb-2">No Mnemonic bookmarks found</p>
132          <p class="text-xs">Export a workspace to bookmarks first, then import it here.</p>
133        </div>
134      {:else}
135        <!-- Folder list -->
136        <div class="space-y-2">
137          <p class="text-sm text-text-muted">Select a folder from your Mnemonic bookmarks:</p>
138          <div class="max-h-48 overflow-y-auto border border-phosphor-dark/20 rounded-lg divide-y divide-phosphor-dark/10">
139            {#each folders as folder (folder.id)}
140              <button
141                class="w-full p-3 text-left hover:bg-phosphor/10 transition-colors flex items-center gap-3 {selectedFolder?.id === folder.id ? 'bg-phosphor/20' : ''}"
142                onclick={() => selectFolder(folder)}
143              >
144                <svg class="w-5 h-5 text-phosphor flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
145                  <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
146                </svg>
147                <div class="flex-1 min-w-0">
148                  <div class="font-medium truncate">{folder.title}</div>
149                  <div class="text-xs text-text-muted flex items-center gap-2">
150                    <span>{formatBookmarkCount(folder.structure.bookmarkCount)}</span>
151                    {#if folder.structure.subfolderCount > 0}
152                      <span class="text-phosphor-dark">|</span>
153                      <span>{folder.structure.subfolderCount} subfolder{folder.structure.subfolderCount !== 1 ? 's' : ''}</span>
154                    {/if}
155                  </div>
156                </div>
157                <span class="px-2 py-0.5 text-xs rounded-full {folder.structure.type === 'nested' ? 'bg-phosphor/20 text-phosphor' : 'bg-surface-raised text-text-muted'}">
158                  {folder.structure.type === 'nested' ? 'Context' : 'Flat'}
159                </span>
160              </button>
161            {/each}
162          </div>
163        </div>
164  
165        <!-- Import options (shown when folder selected) -->
166        {#if selectedFolder}
167          <div class="border-t border-phosphor-dark/20 pt-4 space-y-4">
168            <!-- Structure preview -->
169            <div class="bg-mnemonic-bg-dark/50 rounded-lg p-3">
170              <div class="text-sm font-medium mb-2">Structure Preview</div>
171              <div class="text-xs text-text-muted space-y-1">
172                {#if selectedFolder.structure.type === 'nested'}
173                  <div class="flex items-center gap-2">
174                    <svg class="w-4 h-4 text-phosphor" fill="currentColor" viewBox="0 0 20 20">
175                      <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
176                    </svg>
177                    <span class="font-medium">{selectedFolder.title}</span>
178                    <span class="text-text-muted">(Parent)</span>
179                  </div>
180                  {#each selectedFolder.structure.subfolders as subfolder}
181                    <div class="flex items-center gap-2 ml-6">
182                      <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
183                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
184                      </svg>
185                      <span>{subfolder.name}</span>
186                      <span class="text-text-muted">({subfolder.bookmarkCount} tabs)</span>
187                    </div>
188                  {/each}
189                {:else}
190                  <div class="flex items-center gap-2">
191                    <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
192                      <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 011.523-1.943A5.977 5.977 0 0116 10c0 .34-.028.675-.083 1H15a2 2 0 00-2 2v2.197A5.973 5.973 0 0110 16v-2a2 2 0 00-2-2 2 2 0 01-2-2 2 2 0 00-1.668-1.973z" clip-rule="evenodd" />
193                    </svg>
194                    <span class="font-medium">{selectedFolder.title}</span>
195                    <span>({selectedFolder.structure.bookmarkCount} tabs)</span>
196                  </div>
197                {/if}
198              </div>
199            </div>
200  
201            <!-- Import mode -->
202            <div class="space-y-2">
203              <label class="text-sm font-medium">Import as:</label>
204              <div class="grid grid-cols-3 gap-2">
205                <button
206                  class="p-2 rounded border text-sm transition-colors text-center {importMode === 'auto' ? 'border-phosphor bg-phosphor/10' : 'border-phosphor-dark/30'}"
207                  onclick={() => importMode = 'auto'}
208                >
209                  <div class="font-medium">Auto</div>
210                  <div class="text-xs text-text-muted">
211                    {selectedFolder.recommendedMode === 'context' ? 'Context' : 'Standalone'}
212                  </div>
213                </button>
214                <button
215                  class="p-2 rounded border text-sm transition-colors text-center {importMode === 'standalone' ? 'border-phosphor bg-phosphor/10' : 'border-phosphor-dark/30'}"
216                  onclick={() => importMode = 'standalone'}
217                >
218                  <div class="font-medium">Standalone</div>
219                  <div class="text-xs text-text-muted">Single workspace</div>
220                </button>
221                <button
222                  class="p-2 rounded border text-sm transition-colors text-center {importMode === 'context' ? 'border-phosphor bg-phosphor/10' : 'border-phosphor-dark/30'}"
223                  onclick={() => importMode = 'context'}
224                >
225                  <div class="font-medium">Context</div>
226                  <div class="text-xs text-text-muted">Parent + children</div>
227                </button>
228              </div>
229            </div>
230  
231            <!-- Optional: Import as child of existing parent -->
232            {#if getEffectiveMode() === 'standalone' && parentWorkspaces.length > 0}
233              <div class="space-y-2">
234                <label class="text-sm font-medium flex items-center gap-2">
235                  <span>Add to existing context</span>
236                  <span class="text-xs text-text-muted">(optional)</span>
237                </label>
238                <select
239                  bind:value={selectedParentId}
240                  class="w-full px-3 py-2 rounded bg-mnemonic-bg border border-phosphor-dark/30 text-sm focus:border-phosphor focus:outline-none"
241                >
242                  <option value="">Create as standalone</option>
243                  {#each parentWorkspaces as parent (parent.id)}
244                    <option value={parent.id}>Add to "{parent.name}"</option>
245                  {/each}
246                </select>
247              </div>
248            {/if}
249          </div>
250        {/if}
251      {/if}
252    </div>
253  
254    {#snippet footer()}
255      <Button variant="ghost" onclick={onclose} disabled={importing}>
256        Cancel
257      </Button>
258      <Button
259        variant="primary"
260        onclick={handleImport}
261        disabled={!selectedFolder || importing || loading}
262      >
263        {#if importing}
264          <svg class="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 24 24">
265            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
266            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
267          </svg>
268          Importing...
269        {:else}
270          Import
271        {/if}
272      </Button>
273    {/snippet}
274  </Modal>