mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 13:12:23 +00:00
feat: add Progressive Web App support with service worker and offline functionality
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -99,12 +99,19 @@ Complete feature set for lightweight Vega-Lite snippet management.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Fixed
|
||||||
- (Bugfixes will be listed here)
|
- (Bugfixes will be listed here)
|
||||||
|
|
||||||
### Added
|
|
||||||
- (New features will be listed here)
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- (Improvements and refinements will be listed here)
|
- (Improvements and refinements will be listed here)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
- **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
|
- **Draft/published workflow**: Experiment safely without losing your working version
|
||||||
- **Dataset library**: Store and reuse datasets across snippets (JSON, CSV, TSV, TopoJSON)
|
- **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
|
- **Import/export**: Back up your work or move it between browsers
|
||||||
- **Search and ordering**: Find snippets by name, comment, or spec content
|
- **Search and ordering**: Find snippets by name, comment, or spec content
|
||||||
- **Configurable settings**: Editor options, performance tuning, date formatting, light/dark themes
|
- **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.
|
- **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.
|
- **Experimental dark theme**: Has minor visibility issues in some UI components.
|
||||||
- **No cross-device sync**: Data doesn't sync between browsers or devices.
|
- **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!
|
### We'd Love Your Feedback!
|
||||||
|
|
||||||
|
|||||||
BIN
icon-192x192.png
Normal file
BIN
icon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
icon-512x512.png
Normal file
BIN
icon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
12
index.html
12
index.html
@@ -8,6 +8,15 @@
|
|||||||
<link rel="stylesheet" href="src/styles.css">
|
<link rel="stylesheet" href="src/styles.css">
|
||||||
<link rel="icon" type="image/svg+xml" href="src/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="src/favicon.svg" />
|
||||||
|
|
||||||
|
<!-- PWA Manifest -->
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
<meta name="theme-color" content="#000080">
|
||||||
|
|
||||||
|
<!-- iOS PWA Support -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Astrolabe">
|
||||||
|
<link rel="apple-touch-icon" href="/icon-192x192.png">
|
||||||
|
|
||||||
<!-- Google Fonts - IBM Plex Mono -->
|
<!-- Google Fonts - IBM Plex Mono -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
@@ -526,6 +535,8 @@
|
|||||||
<p class="help-text">
|
<p class="help-text">
|
||||||
Everything runs locally in your browser—no server, no signup, no data leaving your machine.
|
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.
|
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.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -536,6 +547,7 @@
|
|||||||
<li><strong>Three-panel workspace</strong> — Snippet library, Monaco code editor with Vega-Lite schema validation, and live preview</li>
|
<li><strong>Three-panel workspace</strong> — Snippet library, Monaco code editor with Vega-Lite schema validation, and live preview</li>
|
||||||
<li><strong>Draft/published workflow</strong> — Experiment safely without losing your working version</li>
|
<li><strong>Draft/published workflow</strong> — Experiment safely without losing your working version</li>
|
||||||
<li><strong>Dataset library</strong> — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)</li>
|
<li><strong>Dataset library</strong> — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)</li>
|
||||||
|
<li><strong>Offline-capable</strong> — Works without internet connection after first visit; install as standalone app</li>
|
||||||
<li><strong>Import/export</strong> — Back up your work or move it between browsers</li>
|
<li><strong>Import/export</strong> — Back up your work or move it between browsers</li>
|
||||||
<li><strong>Inline data extraction</strong> — Convert hardcoded data into reusable datasets</li>
|
<li><strong>Inline data extraction</strong> — Convert hardcoded data into reusable datasets</li>
|
||||||
<li><strong>Search and sorting</strong> — Find snippets by name, comment, or spec content</li>
|
<li><strong>Search and sorting</strong> — Find snippets by name, comment, or spec content</li>
|
||||||
|
|||||||
30
manifest.webmanifest
Normal file
30
manifest.webmanifest
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu
|
|||||||
## Design Principles
|
## Design Principles
|
||||||
|
|
||||||
- **Local-first**: All data stored in browser (localStorage for snippets, IndexedDB for datasets)
|
- **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
|
- **Minimal dependencies**: Vanilla JavaScript, no build tools, direct CDN imports
|
||||||
- **Developer-friendly**: Full JSON schema support, syntax validation, and intellisense
|
- **Developer-friendly**: Full JSON schema support, syntax validation, and intellisense
|
||||||
- **Version-aware**: Draft/published workflow for safe experimentation
|
- **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)
|
- **Editor**: Monaco Editor v0.47.0 (via CDN)
|
||||||
- **Visualization**: Vega-Embed v6 (includes Vega v5 & Vega-Lite v5)
|
- **Visualization**: Vega-Embed v6 (includes Vega v5 & Vega-Lite v5)
|
||||||
- **Storage**: localStorage (snippets) + IndexedDB (datasets)
|
- **Storage**: localStorage (snippets) + IndexedDB (datasets)
|
||||||
|
- **Offline**: Service Worker API with Cache API for PWA functionality
|
||||||
- **Architecture**: Modular script organization with logical file separation
|
- **Architecture**: Modular script organization with logical file separation
|
||||||
- **Backend**: None (frontend-only application)
|
- **Backend**: None (frontend-only application)
|
||||||
|
|
||||||
@@ -153,6 +155,10 @@ astrolabe:settings # User preferences and UI state
|
|||||||
```
|
```
|
||||||
web/
|
web/
|
||||||
├── index.html # Main HTML structure
|
├── 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/
|
├── src/
|
||||||
│ ├── js/
|
│ ├── js/
|
||||||
│ │ ├── config.js # Global variables, settings API, utilities
|
│ │ ├── config.js # Global variables, settings API, utilities
|
||||||
@@ -230,14 +236,22 @@ web/
|
|||||||
- Performance tuning options
|
- Performance tuning options
|
||||||
- Settings modal UI logic
|
- Settings modal UI logic
|
||||||
|
|
||||||
**app.js** (~250 lines)
|
**app.js** (~270 lines)
|
||||||
- Application initialization sequence
|
- Application initialization sequence
|
||||||
|
- Service worker registration for PWA
|
||||||
- Event listener registration
|
- Event listener registration
|
||||||
- Monaco editor setup
|
- Monaco editor setup
|
||||||
- URL state management (hashchange listener)
|
- URL state management (hashchange listener)
|
||||||
- Keyboard shortcut handlers
|
- Keyboard shortcut handlers
|
||||||
- Modal management
|
- 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)
|
**styles.css** (~280 lines)
|
||||||
- Windows 2000 aesthetic (classic gray, beveled borders)
|
- Windows 2000 aesthetic (classic gray, beveled borders)
|
||||||
- Component-based architecture (base classes + modifiers)
|
- Component-based architecture (base classes + modifiers)
|
||||||
@@ -363,6 +377,22 @@ const URLState = {
|
|||||||
- Page refresh preserves user context
|
- Page refresh preserves user context
|
||||||
- Multi-tab workflows supported
|
- 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
|
### Type Detection Algorithm
|
||||||
|
|
||||||
**Column Type Inference** (for table preview):
|
**Column Type Inference** (for table preview):
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
// Application initialization and event handlers
|
// 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 () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Initialize user settings
|
// Initialize user settings
|
||||||
initSettings();
|
initSettings();
|
||||||
|
|||||||
82
sw.js
Normal file
82
sw.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user