Files
astrolabe-nvc/index.html

544 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Astrolabe - Vega-Lite Snippet Manager</title>
<link rel="stylesheet" href="src/styles.css">
<!-- Monaco Editor -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs/loader.js"></script>
</head>
<body>
<!-- Header -->
<div class="header">
<div class="header-left">
<span class="header-icon">🔭</span>
<span class="header-title">Astrolabe</span>
</div>
<div class="header-links">
<span class="header-link" id="import-link" title="Import snippets from JSON file">Import</span>
<span class="header-link" id="export-link" title="Export all snippets to JSON file">Export</span>
<span class="header-link" id="datasets-link" title="Open dataset manager (Cmd/Ctrl+K)">Datasets</span>
<span class="header-link" id="help-link" title="View keyboard shortcuts and help">Help</span>
<span class="header-link" id="donate-link" title="Support Astrolabe creators">Donate 🇺🇦</span>
<input type="file" id="import-file-input" accept=".json" style="display: none;" />
</div>
</div>
<div class="app-container">
<!-- Toggle Button Strip -->
<div class="toggle-strip">
<button class="btn btn-icon xlarge active" id="toggle-snippet-panel" title="Toggle Snippets Panel">
📄
</button>
<button class="btn btn-icon xlarge active" id="toggle-editor-panel" title="Toggle Editor Panel">
✏️
</button>
<button class="btn btn-icon xlarge active" id="toggle-preview-panel" title="Toggle Preview Panel">
👁️
</button>
<button class="btn btn-icon xlarge" id="toggle-datasets" title="Datasets">
📁
</button>
</div>
<div class="main-panels">
<!-- Snippet Library Panel -->
<div class="panel snippet-panel" id="snippet-panel">
<div class="panel-header">
Snippets
</div>
<div class="sort-controls">
<span class="sort-label">Sort by:</span>
<button class="sort-btn active" data-sort="modified" title="Sort by last modified date">
<span class="sort-text">Modified</span>
<span class="sort-arrow"></span>
</button>
<button class="sort-btn" data-sort="created" title="Sort by creation date">
<span class="sort-text">Created</span>
<span class="sort-arrow"></span>
</button>
<button class="sort-btn" data-sort="name" title="Sort alphabetically by name">
<span class="sort-text">Name</span>
<span class="sort-arrow"></span>
</button>
</div>
<div class="search-controls">
<input type="text" id="snippet-search" placeholder="Search snippets..." />
<button class="btn btn-icon" id="search-clear" title="Clear search">×</button>
</div>
<div class="panel-content">
<ul class="snippet-list">
<!-- Dynamically populated by renderSnippetList() -->
</ul>
<div class="placeholder">
Click to select a snippet
</div>
<div class="snippet-meta" id="snippet-meta" style="display: none;">
<div class="meta-header">Name</div>
<input type="text" id="snippet-name" class="input small" placeholder="Snippet name..." />
<div class="meta-header">Comment</div>
<textarea id="snippet-comment" class="input textarea medium" placeholder="Add a comment..." rows="3"></textarea>
<div class="meta-info">
<div class="meta-info-item">
<span class="meta-info-label">Created:</span>
<span id="snippet-created"></span>
</div>
<div class="meta-info-item">
<span class="meta-info-label">Modified:</span>
<span id="snippet-modified"></span>
</div>
</div>
<div id="snippet-datasets-section" style="display: none;">
<div class="meta-header">Linked Datasets</div>
<div class="meta-info" id="snippet-datasets">
<!-- Dynamically populated by updateLinkedDatasets() -->
</div>
</div>
<div class="meta-actions">
<button class="btn btn-standard flex" id="duplicate-btn" title="Create a copy of this snippet">Duplicate</button>
<button class="btn btn-standard flex danger" id="delete-btn" title="Delete this snippet permanently">Delete</button>
</div>
</div>
<div class="storage-monitor" id="storage-monitor">
<div class="storage-info">
<span class="storage-label">Storage:</span>
<span class="storage-text" id="storage-text">0 KB / 5 MB</span>
</div>
<div class="storage-bar">
<div class="storage-fill" id="storage-fill"></div>
</div>
</div>
</div>
</div>
<!-- Resize Handle 1 -->
<div class="resize-handle" id="resize-handle-1"></div>
<!-- Editor Panel -->
<div class="panel editor-panel" id="editor-panel">
<div class="panel-header">
<span>Editor</span>
<div class="editor-controls">
<button class="btn btn-action" id="extract-btn" style="display: none; background: #87CEEB;" title="Extract inline data to a reusable dataset">Extract to Dataset</button>
<button class="btn btn-action publish" id="publish-btn" title="Publish draft changes (Cmd/Ctrl+S)">Publish</button>
<button class="btn btn-action revert" id="revert-btn" title="Discard draft and revert to published version">Revert</button>
<span class="view-label">View:</span>
<div class="view-toggle-group">
<button class="btn btn-toggle active" id="view-draft" title="View and edit draft version">Draft</button>
<button class="btn btn-toggle" id="view-published" title="View published version (read-only if draft exists)">Published</button>
</div>
</div>
</div>
<div class="panel-content">
<div id="monaco-editor" style="height: 100%; width: 100%;"></div>
</div>
</div>
<!-- Resize Handle 2 -->
<div class="resize-handle" id="resize-handle-2"></div>
<!-- Preview Panel -->
<div class="panel preview-panel" id="preview-panel">
<div class="panel-header">
Preview
</div>
<div class="panel-content">
<div id="vega-preview" style="height: 100%; width: 100%; overflow: auto;"></div>
</div>
</div>
</div>
</div>
<!-- Dataset Manager Modal -->
<div id="dataset-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<span class="modal-title">Dataset Manager</span>
<button class="btn btn-icon" id="dataset-modal-close" title="Close dataset manager (Escape)">×</button>
</div>
<div class="modal-body">
<!-- List View (default) -->
<div id="dataset-list-view" class="dataset-view">
<div class="dataset-list-header">
<button class="btn btn-modal primary" id="new-dataset-btn" title="Create a new dataset">New Dataset</button>
<button class="btn btn-modal" id="import-dataset-btn" title="Import dataset from file">Import</button>
<input type="file" id="import-dataset-file" accept=".json,.csv,.tsv,.txt" style="display: none;" />
</div>
<div class="dataset-container">
<div class="dataset-list" id="dataset-list">
<!-- Dynamically populated by renderDatasetList() -->
</div>
<div class="dataset-details" id="dataset-details" style="display: none;">
<div class="dataset-detail-section">
<div class="dataset-actions">
<button class="btn btn-modal primary" id="new-snippet-btn" title="Create a new snippet using this dataset">New Snippet</button>
<button class="btn btn-modal" id="export-dataset-btn" title="Export this dataset to file">Export</button>
<button class="btn btn-modal" id="copy-reference-btn" title="Copy dataset reference to clipboard">Copy Reference</button>
<button class="btn btn-modal danger" id="delete-dataset-btn" title="Delete this dataset permanently">Delete</button>
</div>
<div class="dataset-detail-header">Name</div>
<input type="text" id="dataset-detail-name" class="input" placeholder="Dataset name..." />
<div class="dataset-detail-header">Comment</div>
<textarea id="dataset-detail-comment" class="input textarea" placeholder="Add a comment..." rows="3"></textarea>
<div class="dataset-detail-header-row">
<span class="dataset-detail-header">Statistics</span>
<button class="btn btn-icon large" id="refresh-metadata-btn" style="display: none;" title="Refresh metadata from URL">🔄</button>
</div>
<div class="stats-box">
<div class="stat-item">
<span class="stat-label">Rows:</span>
<span id="dataset-detail-rows">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Columns:</span>
<span id="dataset-detail-columns">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Size:</span>
<span id="dataset-detail-size">0 B</span>
</div>
</div>
<div class="dataset-detail-header">Timestamps</div>
<div class="stats-box">
<div class="stat-item">
<span class="stat-label">Created:</span>
<span id="dataset-detail-created">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Modified:</span>
<span id="dataset-detail-modified">-</span>
</div>
</div>
<div class="dataset-detail-header-row">
<span class="dataset-detail-header">Preview</span>
<div class="preview-toggle-group" id="preview-toggle-group" style="display: none;">
<button class="btn btn-toggle small active" id="preview-raw-btn" title="Show raw data preview">Raw</button>
<button class="btn btn-toggle small" id="preview-table-btn" title="Show data in table format with type detection">Table</button>
</div>
</div>
<pre id="dataset-preview" class="preview-box large"></pre>
<div id="dataset-preview-table" class="preview-table-container" style="display: none;"></div>
<div id="dataset-snippets-section" style="display: none;">
<div class="dataset-detail-header">Linked Snippets</div>
<div class="stats-box" id="dataset-snippets">
<!-- Dynamically populated by updateLinkedSnippets() -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Form View (for creating new datasets) -->
<div id="dataset-form-view" class="dataset-view" style="display: none;">
<div class="dataset-form">
<div class="dataset-form-header">Create New Dataset</div>
<div class="dataset-form-group">
<label class="dataset-form-label">Name *</label>
<input type="text" id="dataset-form-name" class="input" placeholder="Enter dataset name..." />
</div>
<div class="dataset-form-group">
<label class="dataset-form-label">Data or URL *</label>
<div class="dataset-format-hint">
Paste your data (JSON, CSV, or TSV) or a URL. Format will be detected automatically.
</div>
<textarea id="dataset-form-input" class="input textarea" placeholder="Paste data or URL here..." rows="12"></textarea>
</div>
<!-- Detection Confirmation UI -->
<div id="dataset-detection-confirm" class="dataset-detection-confirm" style="display: none;">
<div class="detection-header">
<span class="detection-title">Detected:</span>
<div class="detection-badges">
<span class="detection-badge" id="detected-format">JSON</span>
<span class="detection-badge" id="detected-source">Inline</span>
<span class="detected-confidence high" id="detected-confidence">high confidence</span>
</div>
</div>
<div class="detection-preview-label">Preview:</div>
<pre id="detected-preview" class="preview-box medium"></pre>
</div>
<div class="dataset-form-group">
<label class="dataset-form-label">Comment</label>
<textarea id="dataset-form-comment" class="input textarea" placeholder="Optional description..." rows="3"></textarea>
</div>
<div class="dataset-form-error" id="dataset-form-error"></div>
<div class="dataset-form-actions">
<button class="btn btn-modal primary" id="save-dataset-btn" title="Save this dataset">Save Dataset</button>
<button class="btn btn-modal" id="cancel-dataset-btn" title="Cancel and return to dataset list">Cancel</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Extract to Dataset Modal -->
<div id="extract-modal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 600px; height: auto; max-height: 80vh;">
<div class="modal-header">
<span class="modal-title">Extract to Dataset</span>
<button class="btn btn-icon" id="extract-modal-close" title="Close modal (Escape)">×</button>
</div>
<div class="modal-body">
<div style="padding: 16px;">
<div class="dataset-form-group">
<label class="dataset-form-label">Dataset Name *</label>
<input type="text" id="extract-dataset-name" class="input" placeholder="Enter dataset name..." />
</div>
<div class="dataset-form-group">
<label class="dataset-form-label">Data Preview</label>
<pre id="extract-data-preview" class="preview-box large" style="max-height: 250px;"></pre>
</div>
<div class="dataset-form-error" id="extract-form-error"></div>
<div class="dataset-form-actions">
<button class="btn btn-modal primary" id="extract-create-btn" title="Create dataset and update snippet reference">Create Dataset</button>
<button class="btn btn-modal" id="extract-cancel-btn" title="Cancel extraction">Cancel</button>
</div>
</div>
</div>
</div>
</div>
<!-- Help Modal -->
<div id="help-modal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 700px; height: auto; max-height: 85vh;">
<div class="modal-header">
<span class="modal-title">Help</span>
<button class="btn btn-icon" id="help-modal-close" title="Close help (Escape)">×</button>
</div>
<div class="modal-body">
<div class="help-content">
<!-- About -->
<section class="help-section">
<h3 class="help-heading">About Astrolabe</h3>
<p class="help-text">
Astrolabe is a lightweight, browser-based snippet manager for Vega-Lite visualizations.
It's designed to help you quickly create, organize, and iterate on visualization specs without
the overhead of a full development environment.
</p>
<p class="help-text">
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.
</p>
</section>
<!-- Key Features -->
<section class="help-section">
<h3 class="help-heading">Key Features</h3>
<ul class="help-list">
<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>Dataset library</strong> — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)</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>Search and sorting</strong> — Find snippets by name, comment, or spec content</li>
</ul>
</section>
<!-- Quick Start -->
<section class="help-section">
<h3 class="help-heading">Getting Started</h3>
<div class="help-workflow">
<div class="help-step">
<strong>1. Create a snippet</strong>
<p>Click the "Create New Snippet" ghost card at the top of the snippet list. A sample Vega-Lite spec loads automatically.</p>
</div>
<div class="help-step">
<strong>2. Edit in the Draft view</strong>
<p>Changes auto-save as you type. The preview updates automatically. Your published version stays safe until you're ready.</p>
</div>
<div class="help-step">
<strong>3. Publish when ready</strong>
<p>Click "Publish" (or Cmd/Ctrl+S) to save your draft as the official version. Use "Revert" if you want to discard changes.</p>
</div>
<div class="help-step">
<strong>4. Organize with datasets</strong>
<p>Open the Dataset Manager to create reusable datasets. Reference them in your specs using <code>{"data": {"name": "dataset-name"}}</code>.</p>
</div>
</div>
</section>
<!-- Keyboard Shortcuts -->
<section class="help-section">
<h3 class="help-heading">Keyboard Shortcuts</h3>
<table class="help-shortcuts-table">
<tbody>
<tr>
<td class="shortcut-key">Cmd/Ctrl+Shift+N</td>
<td class="shortcut-desc">Create new snippet</td>
</tr>
<tr>
<td class="shortcut-key">Cmd/Ctrl+K</td>
<td class="shortcut-desc">Toggle dataset manager</td>
</tr>
<tr>
<td class="shortcut-key">Cmd/Ctrl+S</td>
<td class="shortcut-desc">Publish draft (save)</td>
</tr>
<tr>
<td class="shortcut-key">Escape</td>
<td class="shortcut-desc">Close any open modal</td>
</tr>
</tbody>
</table>
</section>
<!-- Storage & Limits -->
<section class="help-section">
<h3 class="help-heading">Storage & Limits</h3>
<div class="help-warning">
<strong>⚠️ Important:</strong> All data is stored in your browser's local storage. If you clear your browser cache or site data,
your snippets and datasets will be permanently deleted. Regular exports are recommended as backups.
</div>
<ul class="help-list">
<li><strong>Snippets</strong> — Stored in localStorage with a 5 MB limit (shared across all snippets). The storage monitor shows current usage.</li>
<li><strong>Datasets</strong> — Stored in IndexedDB with effectively unlimited space (browser-dependent, typically 50 MB+).</li>
<li><strong>Backup</strong> — Use Import/Export to save your work as JSON files. Datasets can be exported individually from the Dataset Manager.</li>
</ul>
</section>
<!-- Tips & Tricks -->
<section class="help-section">
<h3 class="help-heading">Tips & Tricks</h3>
<ul class="help-list">
<li><strong>Extract inline data</strong> — When editing a spec with inline data, click "Extract to Dataset" to create a reusable dataset automatically.</li>
<li><strong>Dataset references</strong> — Astrolabe resolves dataset references at render time, so you can freely switch between inline and referenced data.</li>
<li><strong>Search across specs</strong> — The search box looks inside snippet names, comments, and the spec content itself.</li>
<li><strong>Linked datasets</strong> — The metadata panel shows which datasets a snippet uses, and the Dataset Manager shows which snippets reference each dataset.</li>
<li><strong>URL datasets</strong> — Reference remote data by URL. Astrolabe fetches and caches it for preview, but the URL is what gets stored.</li>
</ul>
</section>
</div>
</div>
</div>
</div>
<!-- Donate Modal -->
<div id="donate-modal" class="modal" style="display: none;">
<div class="modal-content" style="height: auto;">
<div class="modal-header">
<span class="modal-title"></span>
<button class="btn btn-icon" id="donate-modal-close" title="Close (Escape)">×</button>
</div>
<div class="modal-body">
<div class="help-content">
<section class="help-section">
<h3 class="help-heading">Why Donate?</h3>
<p class="help-text">
Astrolabe is a free open-source project built by a Ukrainian in Kyiv, Ukraine.
If you're thinking of donating, I should be transparent about where that money actually goes - it doesn't go to me or the project.
It goes to something I care about much more. Right now, while I've been building this passion project, there are people I know - my relatives, friends, colleagues - who took arms and joined Ukraine's Armed Forces. They're defending their country against Russian invasion.
Most of them were civilians before this. They're still civilians, really, just doing something they had to do.
I feel deep gratitude to them. So my goal is to support these brave people however I can.
</p>
<p class="help-text">
You might wonder if the military really needs donations.
The honest answer: yes. The government and Ministry of Defense cover basics, but when you're on the frontlines, every small thing matters.
Better equipment, better protection, better conditions - these things can make a difference between life and death.
</p>
<p class="help-text">
If you have concerns about donating to the military: I get it. Not everyone is comfortable with that for various reasons, and I respect that.
The good news is that many of the organizations below also run humanitarian projects for civilians affected by the war. So you have options.
</p>
</section>
<div class="donate-two-column">
<section class="help-section">
<h3 class="help-heading">Where to Donate</h3>
<p class="help-text">
<ul class="help-list">
<li>
<strong><a href="https://bank.gov.ua/en/news/all/natsionalniy-bank-vidkriv-spetsrahunok-dlya-zboru-koshtiv-na-potrebi-armiyi">
National Bank of Ukraine's special account for Ukraine's Armed Forces
</a> - Direct government channel</strong>
</li>
<li>
<strong><a href="https://savelife.in.ua/en/donate-en/#donate-army-card-once">
Come Back Alive Foundation</a></strong> - One of the biggest and most trusted. They've been around since 2014 and share <a href="https://savelife.in.ua/en/reporting-en/">detailed reports</a> of where money goes.
</li>
<li>
<strong><a href="https://macpaw.foundation/">
MacPaw Foundation</a></strong> - Founded by MacPaw (where I work). Started in 2016, shifted focus in 2022 to support the Defence Forces.
</li>
<li>
<strong><a href="https://foundation.kse.ua/en/humanitarian-projects/">
KSE Foundation</a></strong>. Kyiv School of Economics (where I teach). Focuses on both education and humanitarian support for people and defenders
</li>
<li>
<strong><a href="https://standforukraine.com/">
Stand for Ukraine</a></strong> - Not a foundation, but an aggregator of reliable organizations.
The list of fundraisers goes beyond military and covers recovery of veterans & victims of war, shelter for the refugees and many more.
</li>
</ul>
</p>
</section>
<section class="help-section">
<h3 class="help-heading">Other Ways to Support</h3>
<p class="help-text">
Not able to donate? You can still help by:
</p>
<ul class="help-list">
<li>Sharing Astrolabe with colleagues and friends</li>
<li>Reporting bugs and suggesting improvements</li>
<li>Contributing code or documentation</li>
<li>Writing about your experience using Astrolabe</li>
</ul>
</section>
</div>
<section class="help-section">
<p class="help-text" style="text-align: center; font-style: italic; color: var(--win-gray-darker);">
Thank you for considering this.
</p>
<p class="help-text" style="text-align: center; font-style: italic; color: var(--win-gray-darker);">
Oleh Omelchenko
</p>
</section>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toast-container"></div>
<script src="src/js/config.js"></script>
<script src="src/js/snippet-manager.js"></script>
<script src="src/js/dataset-manager.js"></script>
<script src="src/js/panel-manager.js"></script>
<script src="src/js/editor.js"></script>
<script src="src/js/app.js"></script>
<!-- GoatCounter Analytics -->
<script data-goatcounter="https://astrolabe.goatcounter.com/count"
data-goatcounter-settings='{"allow_local": true}'
async src="//gc.zgo.at/count.js"></script>
</body>
</html>