diff --git a/CHANGELOG.md b/CHANGELOG.md index 502f470..c072b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,12 +99,19 @@ Complete feature set for lightweight Vega-Lite snippet management. ## [Unreleased] +### Added +- **Progressive Web App (PWA) Support**: Install Astrolabe as standalone app with offline functionality + - Service worker caches all application files and CDN dependencies + - Full offline access after initial load + - Install button in browser for desktop/mobile installation + - Runs in standalone window without browser chrome + - "Add to Home Screen" support on iOS/Android + - Automatic cache updates when app version changes + - Works seamlessly with existing IndexedDB and localStorage + ### Fixed - (Bugfixes will be listed here) -### Added -- (New features will be listed here) - ### Changed - (Improvements and refinements will be listed here) diff --git a/README.md b/README.md index 78d62d5..1951877 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A lightweight, browser-based snippet manager for Vega-Lite visualizations. Organ - **Visual chart builder**: Create charts without writing JSON – select mark type, map fields to encodings, and see live preview - **Draft/published workflow**: Experiment safely without losing your working version - **Dataset library**: Store and reuse datasets across snippets (JSON, CSV, TSV, TopoJSON) +- **Progressive Web App**: Install as standalone app, works fully offline after first visit - **Import/export**: Back up your work or move it between browsers - **Search and ordering**: Find snippets by name, comment, or spec content - **Configurable settings**: Editor options, performance tuning, date formatting, light/dark themes @@ -90,6 +91,7 @@ A lightweight, browser-based snippet manager for Vega-Lite visualizations. Organ - **Storage limits**: Snippets are limited to 5 MB total (shared localStorage). Datasets use IndexedDB and have much higher limits. - **Experimental dark theme**: Has minor visibility issues in some UI components. - **No cross-device sync**: Data doesn't sync between browsers or devices. +- **Offline functionality**: PWA requires initial online visit to cache resources; subsequent visits work offline. ### We'd Love Your Feedback! diff --git a/icon-192x192.png b/icon-192x192.png new file mode 100644 index 0000000..82f6d26 Binary files /dev/null and b/icon-192x192.png differ diff --git a/icon-512x512.png b/icon-512x512.png new file mode 100644 index 0000000..25d2313 Binary files /dev/null and b/icon-512x512.png differ diff --git a/index.html b/index.html index 1bbf359..8138c1f 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,15 @@ + + + + + + + + + @@ -526,6 +535,8 @@

Everything runs locally in your browser—no server, no signup, no data leaving your machine. Your snippets and datasets are stored using browser storage, so they persist across sessions. + As a Progressive Web App, Astrolabe works fully offline after your first visit and can be installed + as a standalone application.

@@ -536,6 +547,7 @@
  • Three-panel workspace — Snippet library, Monaco code editor with Vega-Lite schema validation, and live preview
  • Draft/published workflow — Experiment safely without losing your working version
  • Dataset library — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)
  • +
  • Offline-capable — Works without internet connection after first visit; install as standalone app
  • Import/export — Back up your work or move it between browsers
  • Inline data extraction — Convert hardcoded data into reusable datasets
  • Search and sorting — Find snippets by name, comment, or spec content
  • diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..fb2ed9f --- /dev/null +++ b/manifest.webmanifest @@ -0,0 +1,30 @@ +{ + "name": "Astrolabe - Vega-Lite Snippet Manager", + "short_name": "Astrolabe", + "description": "A lightweight, browser-based snippet manager for Vega-Lite visualizations", + "start_url": "/", + "display": "standalone", + "background_color": "#c0c0c0", + "theme_color": "#000080", + "orientation": "any", + "icons": [ + { + "src": "src/favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/project-docs/architecture.md b/project-docs/architecture.md index b9d2eba..10309f6 100644 --- a/project-docs/architecture.md +++ b/project-docs/architecture.md @@ -14,6 +14,7 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu ## Design Principles - **Local-first**: All data stored in browser (localStorage for snippets, IndexedDB for datasets) +- **Offline-capable**: Progressive Web App with service worker for full offline functionality - **Minimal dependencies**: Vanilla JavaScript, no build tools, direct CDN imports - **Developer-friendly**: Full JSON schema support, syntax validation, and intellisense - **Version-aware**: Draft/published workflow for safe experimentation @@ -28,6 +29,7 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu - **Editor**: Monaco Editor v0.47.0 (via CDN) - **Visualization**: Vega-Embed v6 (includes Vega v5 & Vega-Lite v5) - **Storage**: localStorage (snippets) + IndexedDB (datasets) +- **Offline**: Service Worker API with Cache API for PWA functionality - **Architecture**: Modular script organization with logical file separation - **Backend**: None (frontend-only application) @@ -153,6 +155,10 @@ astrolabe:settings # User preferences and UI state ``` web/ ├── index.html # Main HTML structure +├── manifest.webmanifest # PWA manifest (app metadata, icons, theme) +├── sw.js # Service worker (offline caching) +├── icon-192x192.png # PWA icon (small) +├── icon-512x512.png # PWA icon (large) ├── src/ │ ├── js/ │ │ ├── config.js # Global variables, settings API, utilities @@ -230,14 +236,22 @@ web/ - Performance tuning options - Settings modal UI logic -**app.js** (~250 lines) +**app.js** (~270 lines) - Application initialization sequence +- Service worker registration for PWA - Event listener registration - Monaco editor setup - URL state management (hashchange listener) - Keyboard shortcut handlers - Modal management +**sw.js** (~90 lines) +- Service worker for Progressive Web App functionality +- Cache management (install, activate, fetch events) +- Offline-first strategy with network fallback +- Automatic cache versioning and cleanup +- CDN resource caching (Monaco, Vega, fonts) + **styles.css** (~280 lines) - Windows 2000 aesthetic (classic gray, beveled borders) - Component-based architecture (base classes + modifiers) @@ -363,6 +377,22 @@ const URLState = { - Page refresh preserves user context - Multi-tab workflows supported +### Progressive Web App Implementation + +Astrolabe uses a service worker to provide offline functionality and installability as a Progressive Web App. + +**Implementation**: +- Service worker (`sw.js`) caches all local files and CDN dependencies (Monaco, Vega, fonts) +- Cache-first strategy with network fallback +- Cache versioning tied to app version for automatic updates +- PWA manifest (`manifest.webmanifest`) defines app metadata, icons, and theme + +**Features**: +- Full offline functionality after first visit +- Browser shows install button for standalone app installation +- Automatic cache updates (checks every 60 seconds) +- Works seamlessly with IndexedDB and localStorage + ### Type Detection Algorithm **Column Type Inference** (for table preview): diff --git a/src/js/app.js b/src/js/app.js index fbdd31f..65ed86a 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,5 +1,23 @@ // Application initialization and event handlers +// Register service worker for PWA functionality +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('Service Worker registered:', registration.scope); + + // Check for updates periodically + setInterval(() => { + registration.update(); + }, 60000); // Check every minute + }) + .catch(error => { + console.warn('Service Worker registration failed:', error); + }); + }); +} + document.addEventListener('DOMContentLoaded', function () { // Initialize user settings initSettings(); diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..f768f41 --- /dev/null +++ b/sw.js @@ -0,0 +1,82 @@ +const CACHE_NAME = 'astrolabe-v1.0.0'; +const URLS_TO_CACHE = [ + '/', + '/index.html', + '/src/styles.css', + '/src/favicon.svg', + '/src/js/config.js', + '/src/js/snippet-manager.js', + '/src/js/dataset-manager.js', + '/src/js/chart-builder.js', + '/src/js/panel-manager.js', + '/src/js/editor.js', + '/src/js/user-settings.js', + '/src/js/generic-storage-ui.js', + '/src/js/app.js' +]; + +// CDN URLs to cache +const CDN_URLS = [ + 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs/loader.js', + 'https://unpkg.com/vega@5/build/vega.min.js', + 'https://unpkg.com/vega-lite@5/build/vega-lite.min.js', + 'https://unpkg.com/vega-embed@6/build/vega-embed.min.js', + 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap' +]; + +// Install event - cache all static assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + // Cache local files + const localCachePromise = cache.addAll(URLS_TO_CACHE); + + // Cache CDN files - they'll be cached during runtime via fetch event + // This avoids CORS issues during install phase + return localCachePromise; + }) + ); + // Force the waiting service worker to become active + self.skipWaiting(); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); + // Take control of all pages immediately + return self.clients.claim(); +}); + +// Fetch event - serve from cache, fallback to network +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request).then((response) => { + // Return cached version or fetch from network + return response || fetch(event.request).then((fetchResponse) => { + // Cache successful responses for future use + if (fetchResponse && fetchResponse.status === 200) { + const responseToCache = fetchResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + } + return fetchResponse; + }); + }).catch(() => { + // Offline fallback - return cached index for navigation requests + if (event.request.mode === 'navigate') { + return caches.match('/index.html'); + } + }) + ); +});