diff --git a/CLAUDE.md b/CLAUDE.md index e2e6dd3..1778add 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Instructions for Claude Code when working on this project. ## Current Status -**Completed**: Phases 0-8 (All core functionality including storage monitoring) -**Next**: Phase 9 - Export/Import +**Completed**: Phases 0-9 (All core functionality including import/export) +**Next**: Phase 10 - Dataset Management See `docs/dev-plan.md` for complete roadmap and technical details. diff --git a/docs/dev-plan.md b/docs/dev-plan.md index a3cc675..b8dc88c 100644 --- a/docs/dev-plan.md +++ b/docs/dev-plan.md @@ -170,16 +170,22 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu --- -### **Phase 9: Export/Import** +### **Phase 9: Export/Import** ✅ **COMPLETE** **Goal**: Portability and backup -- [ ] Export single snippet as JSON file (include metadata) -- [ ] Export all snippets as JSON bundle -- [ ] Import snippets from JSON (with conflict resolution) -- [ ] Drag-and-drop import -- [ ] Export published vs draft options +**Deliverables**: +- Export all snippets to JSON file with auto-generated filename (astrolabe-snippets-YYYY-MM-DD.json) +- Import snippets from JSON with automatic format detection +- Support for both Astrolabe native format and external formats +- Automatic field mapping and normalization (createdAt → created, content → spec, draft → draftSpec) +- "imported" tag automatically added to externally imported snippets +- ID conflict resolution with automatic regeneration +- Additive import (merges with existing snippets, no overwrites) +- Success/error feedback with import count +- File picker integration with header Import/Export links +- Snippet size display in list (shows KB for snippets ≥ 1 KB, right-aligned) -**Deliverable**: Full backup/restore capability +**Note**: Drag-and-drop import and selective export options deferred to future phases --- @@ -305,15 +311,15 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu ## Current Status -**Completed**: Phases 0-8 (Storage, UI, editor, rendering, persistence, CRUD, organization, draft/published workflow, storage monitoring) -**Next**: Phase 9 - Export/Import +**Completed**: Phases 0-9 (Storage, UI, editor, rendering, persistence, CRUD, organization, draft/published workflow, storage monitoring, import/export) +**Next**: Phase 10 - Dataset Management **See**: `CLAUDE.md` for concise current state summary --- ## Implemented Features -### Core Capabilities (Phases 0-8) +### Core Capabilities (Phases 0-9) - Three-panel resizable layout with memory and persistence - Monaco Editor v0.47.0 with Vega-Lite v5 schema validation - Live Vega-Lite rendering with debounced updates and error display @@ -326,6 +332,8 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu - Status indicator lights (green/yellow) showing draft state - Context-aware Publish/Revert buttons with color coding - Storage monitoring with visual progress bar and warning states +- Export/Import functionality with format auto-detection +- Snippet size display (right-aligned, shown for ≥ 1 KB) - Retro Windows 2000 aesthetic throughout ### Technical Implementation @@ -338,4 +346,6 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu - **Panel Memory**: localStorage persistence for sizes and visibility across sessions - **Data Model**: Phase 0 schema with `spec` (published) and `draftSpec` (working) fields - **Storage Calculation**: Blob API for accurate byte counting of snippet data -- **Flexbox Layout**: Scrollable snippet list with fixed metadata and storage monitor at bottom \ No newline at end of file +- **Flexbox Layout**: Scrollable snippet list with fixed metadata and storage monitor at bottom +- **Import/Export**: Format detection, field normalization, ID conflict resolution, additive merging +- **Size Display**: Per-snippet size calculation with conditional rendering (≥ 1 KB threshold) \ No newline at end of file diff --git a/index.html b/index.html index 4b77815..b6ff1ed 100644 --- a/index.html +++ b/index.html @@ -19,9 +19,10 @@ Astrolabe diff --git a/src/js/app.js b/src/js/app.js index c6d41ff..d8e17de 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -89,12 +89,33 @@ document.addEventListener('DOMContentLoaded', function () { }); }); - // Header links - show placeholder - document.querySelectorAll('.header-link').forEach(link => { - link.addEventListener('click', function () { + // Header links + const importLink = document.getElementById('import-link'); + const exportLink = document.getElementById('export-link'); + const helpLink = document.getElementById('help-link'); + const importFileInput = document.getElementById('import-file-input'); + + if (importLink && importFileInput) { + importLink.addEventListener('click', function () { + importFileInput.click(); + }); + + importFileInput.addEventListener('change', function () { + importSnippets(this); + }); + } + + if (exportLink) { + exportLink.addEventListener('click', function () { + exportSnippets(); + }); + } + + if (helpLink) { + helpLink.addEventListener('click', function () { alert('Coming soon in a future phase!'); }); - }); + } // View mode toggle buttons document.getElementById('view-draft').addEventListener('click', () => { diff --git a/src/js/snippet-manager.js b/src/js/snippet-manager.js index 4cc0ac0..d8dabd8 100644 --- a/src/js/snippet-manager.js +++ b/src/js/snippet-manager.js @@ -239,6 +239,11 @@ function renderSnippetList(searchQuery = null) { dateText = formatSnippetDate(snippet.modified); } + // Calculate snippet size + const snippetSize = new Blob([JSON.stringify(snippet)]).size; + const sizeKB = snippetSize / 1024; + const sizeHTML = sizeKB >= 1 ? `${sizeKB.toFixed(0)} KB` : ''; + // Determine status: green if no draft changes, yellow if has draft const hasDraft = JSON.stringify(snippet.spec) !== JSON.stringify(snippet.draftSpec); const statusClass = hasDraft ? 'draft' : 'published'; @@ -249,6 +254,7 @@ function renderSnippetList(searchQuery = null) {
${snippet.name}
${dateText}
+ ${sizeHTML}
`; @@ -846,3 +852,125 @@ function updateStorageMonitor() { } } +// Export all snippets to JSON file +function exportSnippets() { + const snippets = SnippetStorage.loadSnippets(); + + if (snippets.length === 0) { + alert('No snippets to export'); + return; + } + + // Create JSON blob + const jsonString = JSON.stringify(snippets, null, 2); + const blob = new Blob([jsonString], { type: 'application/json' }); + + // Create download link + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `astrolabe-snippets-${new Date().toISOString().slice(0, 10)}.json`; + + // Trigger download + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +// Normalize external snippet format to Astrolabe format +function normalizeSnippet(externalSnippet) { + // Check if already in Astrolabe format (has 'created' field as ISO string) + const isAstrolabeFormat = externalSnippet.created && + typeof externalSnippet.created === 'string' && + externalSnippet.created.includes('T'); + + if (isAstrolabeFormat) { + // Already in correct format, just ensure all fields exist + return { + id: externalSnippet.id || generateSnippetId(), + name: externalSnippet.name || generateSnippetName(), + created: externalSnippet.created, + modified: externalSnippet.modified || externalSnippet.created, + spec: externalSnippet.spec || {}, + draftSpec: externalSnippet.draftSpec || externalSnippet.spec || {}, + comment: externalSnippet.comment || "", + tags: externalSnippet.tags || [], + datasetRefs: externalSnippet.datasetRefs || [], + meta: externalSnippet.meta || {} + }; + } + + // External format - map fields + const createdDate = externalSnippet.createdAt ? + new Date(externalSnippet.createdAt).toISOString() : + new Date().toISOString(); + + return { + id: generateSnippetId(), // Generate new ID to avoid conflicts + name: externalSnippet.name || generateSnippetName(), + created: createdDate, + modified: createdDate, + spec: externalSnippet.content || externalSnippet.spec || {}, + draftSpec: externalSnippet.draft || externalSnippet.draftSpec || externalSnippet.content || externalSnippet.spec || {}, + comment: externalSnippet.comment || "", + tags: ["imported"], // Add 'imported' tag + datasetRefs: [], + meta: {} + }; +} + +// Import snippets from JSON file +function importSnippets(fileInput) { + const file = fileInput.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const importedData = JSON.parse(e.target.result); + + // Handle both single snippet and array of snippets + const snippetsToImport = Array.isArray(importedData) ? importedData : [importedData]; + + if (snippetsToImport.length === 0) { + alert('No snippets found in file'); + return; + } + + // Normalize and merge with existing snippets + const existingSnippets = SnippetStorage.loadSnippets(); + const existingIds = new Set(existingSnippets.map(s => s.id)); + + let importedCount = 0; + snippetsToImport.forEach(snippet => { + const normalized = normalizeSnippet(snippet); + + // Ensure no ID conflicts + while (existingIds.has(normalized.id)) { + normalized.id = generateSnippetId(); + } + + existingSnippets.push(normalized); + existingIds.add(normalized.id); + importedCount++; + }); + + // Save all snippets + if (SnippetStorage.saveSnippets(existingSnippets)) { + alert(`Successfully imported ${importedCount} snippet${importedCount !== 1 ? 's' : ''}`); + renderSnippetList(); + } + + } catch (error) { + console.error('Import error:', error); + alert('Failed to import snippets. Please check that the file is valid JSON.'); + } + + // Clear file input + fileInput.value = ''; + }; + + reader.readAsText(file); +} + diff --git a/src/styles.css b/src/styles.css index 7eaced8..3b6d33d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -458,6 +458,14 @@ body { margin-top: 1px; } +.snippet-size { + font-size: 10px; + color: #808080; + margin-left: auto; + margin-right: 8px; + flex-shrink: 0; +} + .editor-placeholder { background: #ffffff; border: 2px inset #c0c0c0;