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');
+ }
+ })
+ );
+});