refactor: Migrate preview fit mode state management and UI updates to an Alpine.js store.

This commit is contained in:
2025-11-25 00:18:08 +02:00
parent 4c6fe5b9bd
commit 3e749a0c13
4 changed files with 363 additions and 255 deletions

View File

@@ -52,25 +52,19 @@
<div class="app-container"> <div class="app-container">
<!-- Toggle Button Strip --> <!-- Toggle Button Strip -->
<div class="toggle-strip" x-data> <div class="toggle-strip" x-data>
<button class="btn btn-icon xlarge" <button class="btn btn-icon xlarge" id="toggle-snippet-panel"
id="toggle-snippet-panel" :class="{ 'active': $store.panels.snippetVisible }" @click="togglePanel('snippet-panel')"
:class="{ 'active': $store.panels.snippetVisible }" title="Toggle Snippets Panel">
@click="togglePanel('snippet-panel')"
title="Toggle Snippets Panel">
📄 📄
</button> </button>
<button class="btn btn-icon xlarge" <button class="btn btn-icon xlarge" id="toggle-editor-panel"
id="toggle-editor-panel" :class="{ 'active': $store.panels.editorVisible }" @click="togglePanel('editor-panel')"
:class="{ 'active': $store.panels.editorVisible }" title="Toggle Editor Panel">
@click="togglePanel('editor-panel')"
title="Toggle Editor Panel">
✏️ ✏️
</button> </button>
<button class="btn btn-icon xlarge" <button class="btn btn-icon xlarge" id="toggle-preview-panel"
id="toggle-preview-panel" :class="{ 'active': $store.panels.previewVisible }" @click="togglePanel('preview-panel')"
:class="{ 'active': $store.panels.previewVisible }" title="Toggle Preview Panel">
@click="togglePanel('preview-panel')"
title="Toggle Preview Panel">
👁️ 👁️
</button> </button>
<button class="btn btn-icon xlarge" id="toggle-datasets" title="Datasets"> <button class="btn btn-icon xlarge" id="toggle-datasets" title="Datasets">
@@ -86,98 +80,72 @@
</div> </div>
<div class="sort-controls"> <div class="sort-controls">
<span class="sort-label">Sort by:</span> <span class="sort-label">Sort by:</span>
<button class="sort-btn" <button class="sort-btn" :class="{ 'active': sortBy === 'modified' }"
:class="{ 'active': sortBy === 'modified' }" @click="toggleSort('modified')" title="Sort by last modified date">
@click="toggleSort('modified')"
title="Sort by last modified date">
<span class="sort-text">Modified</span> <span class="sort-text">Modified</span>
<span class="sort-arrow" x-text="sortBy === 'modified' && sortOrder === 'desc' ? '⬇' : '⬆'"></span> <span class="sort-arrow"
x-text="sortBy === 'modified' && sortOrder === 'desc' ? '⬇' : '⬆'"></span>
</button> </button>
<button class="sort-btn" <button class="sort-btn" :class="{ 'active': sortBy === 'created' }" @click="toggleSort('created')"
:class="{ 'active': sortBy === 'created' }" title="Sort by creation date">
@click="toggleSort('created')"
title="Sort by creation date">
<span class="sort-text">Created</span> <span class="sort-text">Created</span>
<span class="sort-arrow" x-text="sortBy === 'created' && sortOrder === 'desc' ? '⬇' : '⬆'"></span> <span class="sort-arrow"
x-text="sortBy === 'created' && sortOrder === 'desc' ? '⬇' : '⬆'"></span>
</button> </button>
<button class="sort-btn" <button class="sort-btn" :class="{ 'active': sortBy === 'name' }" @click="toggleSort('name')"
:class="{ 'active': sortBy === 'name' }" title="Sort alphabetically by name">
@click="toggleSort('name')"
title="Sort alphabetically by name">
<span class="sort-text">Name</span> <span class="sort-text">Name</span>
<span class="sort-arrow" x-text="sortBy === 'name' && sortOrder === 'desc' ? '⬇' : '⬆'"></span> <span class="sort-arrow" x-text="sortBy === 'name' && sortOrder === 'desc' ? '⬇' : '⬆'"></span>
</button> </button>
<button class="sort-btn" <button class="sort-btn" :class="{ 'active': sortBy === 'size' }" @click="toggleSort('size')"
:class="{ 'active': sortBy === 'size' }" title="Sort by snippet size">
@click="toggleSort('size')"
title="Sort by snippet size">
<span class="sort-text">Size</span> <span class="sort-text">Size</span>
<span class="sort-arrow" x-text="sortBy === 'size' && sortOrder === 'desc' ? '⬇' : '⬆'"></span> <span class="sort-arrow" x-text="sortBy === 'size' && sortOrder === 'desc' ? '⬇' : '⬆'"></span>
</button> </button>
</div> </div>
<div class="search-controls"> <div class="search-controls">
<input type="text" <input type="text" id="snippet-search" x-model="searchQuery" placeholder="Search snippets..." />
id="snippet-search" <button class="btn btn-icon" @click="clearSearch()" title="Clear search">×</button>
x-model="searchQuery"
placeholder="Search snippets..." />
<button class="btn btn-icon"
@click="clearSearch()"
title="Clear search">×</button>
</div> </div>
<div class="panel-content"> <div class="panel-content">
<ul class="snippet-list" id="snippet-list"> <ul class="snippet-list" id="snippet-list">
<!-- Ghost card for creating new snippets --> <!-- Ghost card for creating new snippets -->
<li class="snippet-item ghost-card" <li class="snippet-item ghost-card" id="new-snippet-card" @click="createNewSnippet()">
id="new-snippet-card"
@click="createNewSnippet()">
<div class="snippet-name">+ Create New Snippet</div> <div class="snippet-name">+ Create New Snippet</div>
<div class="snippet-date">Click to create</div> <div class="snippet-date">Click to create</div>
</li> </li>
<!-- Snippet items --> <!-- Snippet items -->
<template x-for="snippet in filteredSnippets" :key="snippet.id"> <template x-for="snippet in filteredSnippets" :key="snippet.id">
<li class="snippet-item" <li class="snippet-item" :data-item-id="snippet.id"
:data-item-id="snippet.id"
:class="{ 'selected': $store.snippets.currentSnippetId === snippet.id }" :class="{ 'selected': $store.snippets.currentSnippetId === snippet.id }"
@click="selectSnippet(snippet.id)"> @click="selectSnippet(snippet.id)">
<div class="snippet-info"> <div class="snippet-info">
<div class="snippet-name"> <div class="snippet-name">
<span x-text="snippet.name"></span> <span x-text="snippet.name"></span>
<span x-show="snippet.datasetRefs && snippet.datasetRefs.length > 0" <span x-show="snippet.datasetRefs && snippet.datasetRefs.length > 0"
class="snippet-dataset-icon" class="snippet-dataset-icon" title="Uses external dataset">📁</span>
title="Uses external dataset">📁</span>
</div> </div>
<div class="snippet-date" x-text="formatDate(snippet)"></div> <div class="snippet-date" x-text="formatDate(snippet)"></div>
</div> </div>
<span x-show="getSize(snippet) >= 1" <span x-show="getSize(snippet) >= 1" class="snippet-size"
class="snippet-size" x-text="getSize(snippet).toFixed(0) + ' KB'"></span>
x-text="getSize(snippet).toFixed(0) + ' KB'"></span> <div class="snippet-status" :class="hasDraft(snippet) ? 'draft' : 'published'"></div>
<div class="snippet-status"
:class="hasDraft(snippet) ? 'draft' : 'published'"></div>
</li> </li>
</template> </template>
</ul> </ul>
<div class="placeholder" <div class="placeholder" x-show="filteredSnippets.length === 0"
x-show="filteredSnippets.length === 0" x-text="searchQuery.trim() ? 'No snippets match your search' : 'No snippets found'">
x-text="searchQuery.trim() ? 'No snippets match your search' : 'No snippets found'">
Click to select a snippet Click to select a snippet
</div> </div>
<div class="snippet-meta" id="snippet-meta" style="display: none;"> <div class="snippet-meta" id="snippet-meta" style="display: none;">
<div class="meta-header">Name</div> <div class="meta-header">Name</div>
<input type="text" <input type="text" id="snippet-name" class="input small" placeholder="Snippet name..."
id="snippet-name" x-model="snippetName" @input="saveMetaDebounced()" />
class="input small"
placeholder="Snippet name..."
x-model="snippetName"
@input="saveMetaDebounced()" />
<div class="meta-header">Comment</div> <div class="meta-header">Comment</div>
<textarea id="snippet-comment" <textarea id="snippet-comment" class="input textarea medium" placeholder="Add a comment..."
class="input textarea medium" rows="3" x-model="snippetComment" @input="saveMetaDebounced()"></textarea>
placeholder="Add a comment..."
rows="3"
x-model="snippetComment"
@input="saveMetaDebounced()"></textarea>
<div class="meta-info"> <div class="meta-info">
<div class="meta-info-item"> <div class="meta-info-item">
@@ -198,8 +166,10 @@
</div> </div>
<div class="meta-actions"> <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" id="duplicate-btn"
<button class="btn btn-standard flex danger" id="delete-btn" title="Delete this snippet permanently">Delete</button> 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> </div>
<div class="storage-monitor" id="storage-monitor"> <div class="storage-monitor" id="storage-monitor">
@@ -222,21 +192,22 @@
<div class="panel-header"> <div class="panel-header">
<span>Editor</span> <span>Editor</span>
<div class="editor-controls"> <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" id="extract-btn" style="display: none; background: #87CEEB;"
<button class="btn btn-action publish" id="publish-btn" title="Publish draft changes (Cmd/Ctrl+S)">Publish</button> title="Extract inline data to a reusable dataset">Extract to Dataset</button>
<button class="btn btn-action revert" id="revert-btn" title="Discard draft and revert to published version">Revert</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> <span class="view-label">View:</span>
<div class="view-toggle-group"> <div class="view-toggle-group">
<button class="btn btn-toggle" <button class="btn btn-toggle" id="view-draft"
id="view-draft" :class="{ 'active': $store.snippets.viewMode === 'draft' }"
:class="{ 'active': $store.snippets.viewMode === 'draft' }" @click="$store.snippets.viewMode = 'draft'; switchViewMode('draft')"
@click="$store.snippets.viewMode = 'draft'; switchViewMode('draft')" title="View and edit draft version">Draft</button>
title="View and edit draft version">Draft</button> <button class="btn btn-toggle" id="view-published"
<button class="btn btn-toggle" :class="{ 'active': $store.snippets.viewMode === 'published' }"
id="view-published" @click="$store.snippets.viewMode = 'published'; switchViewMode('published')"
:class="{ 'active': $store.snippets.viewMode === 'published' }" title="View published version (read-only if draft exists)">Published</button>
@click="$store.snippets.viewMode = 'published'; switchViewMode('published')"
title="View published version (read-only if draft exists)">Published</button>
</div> </div>
</div> </div>
</div> </div>
@@ -252,17 +223,27 @@
<div class="panel preview-panel" id="preview-panel"> <div class="panel preview-panel" id="preview-panel">
<div class="panel-header"> <div class="panel-header">
<span>Preview</span> <span>Preview</span>
<div class="preview-controls"> <div class="preview-controls" x-data>
<span class="view-label">Fit:</span> <span class="view-label">Fit:</span>
<div class="view-toggle-group"> <div class="view-toggle-group">
<button class="btn btn-toggle active" id="preview-fit-default" title="Display at original spec dimensions">Original</button> <button class="btn btn-toggle" :class="{ 'active': $store.preview.fitMode === 'default' }"
<button class="btn btn-toggle" id="preview-fit-width" title="Scale to fit preview pane width (⚠️ for faceted specs, applies to each facet)">Width</button> @click="$store.preview.fitMode = 'default'; setPreviewFitMode('default')"
<button class="btn btn-toggle" id="preview-fit-full" title="Scale to fit entire preview pane (⚠️ for faceted specs, applies to each facet)">Full</button> id="preview-fit-default" title="Display at original spec dimensions">Original</button>
<button class="btn btn-toggle" :class="{ 'active': $store.preview.fitMode === 'width' }"
@click="$store.preview.fitMode = 'width'; setPreviewFitMode('width')"
id="preview-fit-width"
title="Scale to fit preview pane width (⚠️ for faceted specs, applies to each facet)">Width</button>
<button class="btn btn-toggle" :class="{ 'active': $store.preview.fitMode === 'full' }"
@click="$store.preview.fitMode = 'full'; setPreviewFitMode('full')"
id="preview-fit-full"
title="Scale to fit entire preview pane (⚠️ for faceted specs, applies to each facet)">Full</button>
</div> </div>
</div> </div>
</div> </div>
<div class="panel-content" style="position: relative;"> <div class="panel-content" style="position: relative;">
<div id="vega-preview" style="height: 100%; width: 100%; overflow: auto; display: flex; align-items: center; justify-content: center;"></div> <div id="vega-preview"
style="height: 100%; width: 100%; overflow: auto; display: flex; align-items: center; justify-content: center;">
</div>
<div id="preview-overlay" class="preview-overlay" style="display: none;"></div> <div id="preview-overlay" class="preview-overlay" style="display: none;"></div>
</div> </div>
</div> </div>
@@ -280,26 +261,27 @@
<!-- List View (default) --> <!-- List View (default) -->
<div id="dataset-list-view" class="dataset-view" x-data="datasetList()"> <div id="dataset-list-view" class="dataset-view" x-data="datasetList()">
<div class="dataset-list-header"> <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 primary" id="new-dataset-btn" title="Create a new dataset">New
<button class="btn btn-modal" id="import-dataset-btn" title="Import dataset from file">Import</button> Dataset</button>
<input type="file" id="import-dataset-file" accept=".json,.csv,.tsv,.txt" style="display: none;" /> <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>
<div class="dataset-container"> <div class="dataset-container">
<div class="dataset-list" id="dataset-list"> <div class="dataset-list" id="dataset-list">
<!-- Dataset items rendered by Alpine.js --> <!-- Dataset items rendered by Alpine.js -->
<template x-for="dataset in datasets" :key="dataset.id"> <template x-for="dataset in datasets" :key="dataset.id">
<div class="dataset-item" <div class="dataset-item" :data-item-id="dataset.id"
:data-item-id="dataset.id" :class="{ 'selected': $store.datasets.currentDatasetId === dataset.id }"
:class="{ 'selected': $store.datasets.currentDatasetId === dataset.id }" @click="selectDataset(dataset.id)">
@click="selectDataset(dataset.id)">
<div class="dataset-info"> <div class="dataset-info">
<div class="dataset-name" x-text="dataset.name"></div> <div class="dataset-name" x-text="dataset.name"></div>
<div class="dataset-meta" x-text="formatMeta(dataset)"></div> <div class="dataset-meta" x-text="formatMeta(dataset)"></div>
</div> </div>
<div class="dataset-usage-badge" <div class="dataset-usage-badge" x-show="getUsageCount(dataset) > 0"
x-show="getUsageCount(dataset) > 0" :title="getUsageCount(dataset) + ' snippet' + (getUsageCount(dataset) !== 1 ? 's' : '') + ' using this dataset'"
:title="getUsageCount(dataset) + ' snippet' + (getUsageCount(dataset) !== 1 ? 's' : '') + ' using this dataset'" x-text="'📄 ' + getUsageCount(dataset)">
x-text="'📄 ' + getUsageCount(dataset)">
</div> </div>
</div> </div>
</template> </template>
@@ -310,23 +292,32 @@
<div class="dataset-details" id="dataset-details" style="display: none;"> <div class="dataset-details" id="dataset-details" style="display: none;">
<div class="dataset-detail-section"> <div class="dataset-detail-section">
<div class="dataset-actions"> <div class="dataset-actions">
<button class="btn btn-modal primary" id="edit-dataset-btn" title="Edit this dataset contents">Edit Contents</button> <button class="btn btn-modal primary" id="edit-dataset-btn"
<button class="btn btn-modal primary" id="build-chart-btn" title="Build a chart using this dataset">Build Chart</button> title="Edit this dataset contents">Edit Contents</button>
<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 primary" id="build-chart-btn"
<button class="btn btn-modal" id="export-dataset-btn" title="Export this dataset to file">Export</button> title="Build a chart using this dataset">Build Chart</button>
<button class="btn btn-modal" id="copy-reference-btn" title="Copy dataset reference to clipboard">Copy Reference</button> <button class="btn btn-modal primary" id="new-snippet-btn"
<button class="btn btn-modal danger" id="delete-dataset-btn" title="Delete this dataset permanently">Delete</button> 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>
<div class="dataset-detail-header">Name</div> <div class="dataset-detail-header">Name</div>
<input type="text" id="dataset-detail-name" class="input" placeholder="Dataset name..." /> <input type="text" id="dataset-detail-name" class="input"
placeholder="Dataset name..." />
<div class="dataset-detail-header">Comment</div> <div class="dataset-detail-header">Comment</div>
<textarea id="dataset-detail-comment" class="input textarea" placeholder="Add a comment..." rows="3"></textarea> <textarea id="dataset-detail-comment" class="input textarea"
placeholder="Add a comment..." rows="3"></textarea>
<div class="dataset-detail-header-row"> <div class="dataset-detail-header-row">
<span class="dataset-detail-header">Overview</span> <span class="dataset-detail-header">Overview</span>
<button class="btn btn-icon large" id="refresh-metadata-btn" style="display: none;" title="Refresh metadata from URL">🔄</button> <button class="btn btn-icon large" id="refresh-metadata-btn" style="display: none;"
title="Refresh metadata from URL">🔄</button>
</div> </div>
<div class="dataset-overview-grid"> <div class="dataset-overview-grid">
<div class="overview-section"> <div class="overview-section">
@@ -372,12 +363,15 @@
<div class="dataset-detail-header-row"> <div class="dataset-detail-header-row">
<span class="dataset-detail-header">Preview</span> <span class="dataset-detail-header">Preview</span>
<div class="preview-toggle-group" id="preview-toggle-group" style="display: none;"> <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 active" id="preview-raw-btn"
<button class="btn btn-toggle small" id="preview-table-btn" title="Show data in table format with type detection">Table</button> 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>
</div> </div>
<pre id="dataset-preview" class="preview-box large"></pre> <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-preview-table" class="preview-table-container" style="display: none;">
</div>
<div id="dataset-snippets-section" style="display: none;"> <div id="dataset-snippets-section" style="display: none;">
<div class="dataset-detail-header">Linked Snippets</div> <div class="dataset-detail-header">Linked Snippets</div>
@@ -397,7 +391,8 @@
<div class="dataset-form-group"> <div class="dataset-form-group">
<label class="dataset-form-label">Name *</label> <label class="dataset-form-label">Name *</label>
<input type="text" id="dataset-form-name" class="input" placeholder="Enter dataset name..." /> <input type="text" id="dataset-form-name" class="input"
placeholder="Enter dataset name..." />
</div> </div>
<div class="dataset-form-group"> <div class="dataset-form-group">
@@ -405,7 +400,8 @@
<div class="dataset-format-hint"> <div class="dataset-format-hint">
Paste your data (JSON, CSV, or TSV) or a URL. Format will be detected automatically. Paste your data (JSON, CSV, or TSV) or a URL. Format will be detected automatically.
</div> </div>
<textarea id="dataset-form-input" class="input textarea" placeholder="Paste data or URL here..." rows="12"></textarea> <textarea id="dataset-form-input" class="input textarea"
placeholder="Paste data or URL here..." rows="12"></textarea>
</div> </div>
<!-- Detection Confirmation UI --> <!-- Detection Confirmation UI -->
@@ -415,7 +411,8 @@
<div class="detection-badges"> <div class="detection-badges">
<span class="detection-badge" id="detected-format">JSON</span> <span class="detection-badge" id="detected-format">JSON</span>
<span class="detection-badge" id="detected-source">Inline</span> <span class="detection-badge" id="detected-source">Inline</span>
<span class="detected-confidence high" id="detected-confidence">high confidence</span> <span class="detected-confidence high" id="detected-confidence">high
confidence</span>
</div> </div>
</div> </div>
<div class="detection-preview-label">Preview:</div> <div class="detection-preview-label">Preview:</div>
@@ -424,14 +421,17 @@
<div class="dataset-form-group"> <div class="dataset-form-group">
<label class="dataset-form-label">Comment</label> <label class="dataset-form-label">Comment</label>
<textarea id="dataset-form-comment" class="input textarea" placeholder="Optional description..." rows="3"></textarea> <textarea id="dataset-form-comment" class="input textarea"
placeholder="Optional description..." rows="3"></textarea>
</div> </div>
<div class="dataset-form-error" id="dataset-form-error"></div> <div class="dataset-form-error" id="dataset-form-error"></div>
<div class="dataset-form-actions"> <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 primary" id="save-dataset-btn" title="Save this dataset">Save
<button class="btn btn-modal" id="cancel-dataset-btn" title="Cancel and return to dataset list">Cancel</button> 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>
@@ -450,7 +450,8 @@
<div style="padding: 16px;"> <div style="padding: 16px;">
<div class="dataset-form-group"> <div class="dataset-form-group">
<label class="dataset-form-label">Dataset Name *</label> <label class="dataset-form-label">Dataset Name *</label>
<input type="text" id="extract-dataset-name" class="input" placeholder="Enter dataset name..." /> <input type="text" id="extract-dataset-name" class="input"
placeholder="Enter dataset name..." />
</div> </div>
<div class="dataset-form-group"> <div class="dataset-form-group">
@@ -461,7 +462,8 @@
<div class="dataset-form-error" id="extract-form-error"></div> <div class="dataset-form-error" id="extract-form-error"></div>
<div class="dataset-form-actions"> <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 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> <button class="btn btn-modal" id="extract-cancel-btn" title="Cancel extraction">Cancel</button>
</div> </div>
</div> </div>
@@ -474,7 +476,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<span class="modal-title">Build Chart</span> <span class="modal-title">Build Chart</span>
<button class="btn btn-icon" id="chart-builder-modal-close" @click="$el.closest('#chart-builder-view')._x_dataStack[0].close()" title="Close chart builder (Escape)">×</button> <button class="btn btn-icon" id="chart-builder-modal-close"
@click="$el.closest('#chart-builder-view')._x_dataStack[0].close()"
title="Close chart builder (Escape)">×</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<!-- Chart Builder View --> <!-- Chart Builder View -->
@@ -483,18 +487,28 @@
<!-- Left Panel: Configuration --> <!-- Left Panel: Configuration -->
<div class="chart-builder-config"> <div class="chart-builder-config">
<div class="chart-builder-header"> <div class="chart-builder-header">
<button class="btn btn-modal" id="chart-builder-back-btn" @click="close()" title="Back to dataset details">← Back to Dataset</button> <button class="btn btn-modal" id="chart-builder-back-btn" @click="close()"
title="Back to dataset details">← Back to Dataset</button>
</div> </div>
<div class="chart-builder-section"> <div class="chart-builder-section">
<div class="mark-type-row"> <div class="mark-type-row">
<label class="chart-builder-label">Mark Type*</label> <label class="chart-builder-label">Mark Type*</label>
<div class="mark-toggle-group"> <div class="mark-toggle-group">
<button class="btn btn-toggle small" :class="{ 'active': markType === 'bar' }" @click="setMarkType('bar')" data-mark="bar" title="Bar chart">Bar</button> <button class="btn btn-toggle small" :class="{ 'active': markType === 'bar' }"
<button class="btn btn-toggle small" :class="{ 'active': markType === 'line' }" @click="setMarkType('line')" data-mark="line" title="Line chart">Line</button> @click="setMarkType('bar')" data-mark="bar" title="Bar chart">Bar</button>
<button class="btn btn-toggle small" :class="{ 'active': markType === 'point' }" @click="setMarkType('point')" data-mark="point" title="Point chart">Point</button> <button class="btn btn-toggle small" :class="{ 'active': markType === 'line' }"
<button class="btn btn-toggle small" :class="{ 'active': markType === 'area' }" @click="setMarkType('area')" data-mark="area" title="Area chart">Area</button> @click="setMarkType('line')" data-mark="line"
<button class="btn btn-toggle small" :class="{ 'active': markType === 'circle' }" @click="setMarkType('circle')" data-mark="circle" title="Circle chart">Circle</button> title="Line chart">Line</button>
<button class="btn btn-toggle small" :class="{ 'active': markType === 'point' }"
@click="setMarkType('point')" data-mark="point"
title="Point chart">Point</button>
<button class="btn btn-toggle small" :class="{ 'active': markType === 'area' }"
@click="setMarkType('area')" data-mark="area"
title="Area chart">Area</button>
<button class="btn btn-toggle small"
:class="{ 'active': markType === 'circle' }" @click="setMarkType('circle')"
data-mark="circle" title="Circle chart">Circle</button>
</div> </div>
</div> </div>
</div> </div>
@@ -506,17 +520,30 @@
<div class="encoding-group"> <div class="encoding-group">
<div class="encoding-row"> <div class="encoding-row">
<label class="encoding-header">X Axis</label> <label class="encoding-header">X Axis</label>
<select id="encoding-x-field" class="input" x-model="encodings.x.field" @change="setEncodingField('x', $event.target.value)"> <select id="encoding-x-field" class="input" x-model="encodings.x.field"
@change="setEncodingField('x', $event.target.value)">
<option value="">None</option> <option value="">None</option>
</select> </select>
</div> </div>
<div class="encoding-type"> <div class="encoding-type">
<label class="encoding-type-label">Type</label> <label class="encoding-type-label">Type</label>
<div class="type-toggle-group"> <div class="type-toggle-group">
<button class="btn btn-toggle small" :class="{ 'active': encodings.x.type === 'quantitative' }" @click="setEncodingType('x', 'quantitative')" data-encoding="x" data-type="quantitative" title="Quantitative">Q</button> <button class="btn btn-toggle small"
<button class="btn btn-toggle small" :class="{ 'active': encodings.x.type === 'ordinal' }" @click="setEncodingType('x', 'ordinal')" data-encoding="x" data-type="ordinal" title="Ordinal">O</button> :class="{ 'active': encodings.x.type === 'quantitative' }"
<button class="btn btn-toggle small" :class="{ 'active': encodings.x.type === 'nominal' }" @click="setEncodingType('x', 'nominal')" data-encoding="x" data-type="nominal" title="Nominal">N</button> @click="setEncodingType('x', 'quantitative')" data-encoding="x"
<button class="btn btn-toggle small" :class="{ 'active': encodings.x.type === 'temporal' }" @click="setEncodingType('x', 'temporal')" data-encoding="x" data-type="temporal" title="Temporal">T</button> data-type="quantitative" title="Quantitative">Q</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.x.type === 'ordinal' }"
@click="setEncodingType('x', 'ordinal')" data-encoding="x"
data-type="ordinal" title="Ordinal">O</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.x.type === 'nominal' }"
@click="setEncodingType('x', 'nominal')" data-encoding="x"
data-type="nominal" title="Nominal">N</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.x.type === 'temporal' }"
@click="setEncodingType('x', 'temporal')" data-encoding="x"
data-type="temporal" title="Temporal">T</button>
</div> </div>
</div> </div>
</div> </div>
@@ -525,17 +552,30 @@
<div class="encoding-group"> <div class="encoding-group">
<div class="encoding-row"> <div class="encoding-row">
<label class="encoding-header">Y Axis</label> <label class="encoding-header">Y Axis</label>
<select id="encoding-y-field" class="input" x-model="encodings.y.field" @change="setEncodingField('y', $event.target.value)"> <select id="encoding-y-field" class="input" x-model="encodings.y.field"
@change="setEncodingField('y', $event.target.value)">
<option value="">None</option> <option value="">None</option>
</select> </select>
</div> </div>
<div class="encoding-type"> <div class="encoding-type">
<label class="encoding-type-label">Type</label> <label class="encoding-type-label">Type</label>
<div class="type-toggle-group"> <div class="type-toggle-group">
<button class="btn btn-toggle small" :class="{ 'active': encodings.y.type === 'quantitative' }" @click="setEncodingType('y', 'quantitative')" data-encoding="y" data-type="quantitative" title="Quantitative">Q</button> <button class="btn btn-toggle small"
<button class="btn btn-toggle small" :class="{ 'active': encodings.y.type === 'ordinal' }" @click="setEncodingType('y', 'ordinal')" data-encoding="y" data-type="ordinal" title="Ordinal">O</button> :class="{ 'active': encodings.y.type === 'quantitative' }"
<button class="btn btn-toggle small" :class="{ 'active': encodings.y.type === 'nominal' }" @click="setEncodingType('y', 'nominal')" data-encoding="y" data-type="nominal" title="Nominal">N</button> @click="setEncodingType('y', 'quantitative')" data-encoding="y"
<button class="btn btn-toggle small" :class="{ 'active': encodings.y.type === 'temporal' }" @click="setEncodingType('y', 'temporal')" data-encoding="y" data-type="temporal" title="Temporal">T</button> data-type="quantitative" title="Quantitative">Q</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.y.type === 'ordinal' }"
@click="setEncodingType('y', 'ordinal')" data-encoding="y"
data-type="ordinal" title="Ordinal">O</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.y.type === 'nominal' }"
@click="setEncodingType('y', 'nominal')" data-encoding="y"
data-type="nominal" title="Nominal">N</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.y.type === 'temporal' }"
@click="setEncodingType('y', 'temporal')" data-encoding="y"
data-type="temporal" title="Temporal">T</button>
</div> </div>
</div> </div>
</div> </div>
@@ -544,17 +584,30 @@
<div class="encoding-group"> <div class="encoding-group">
<div class="encoding-row"> <div class="encoding-row">
<label class="encoding-header">Color</label> <label class="encoding-header">Color</label>
<select id="encoding-color-field" class="input" x-model="encodings.color.field" @change="setEncodingField('color', $event.target.value)"> <select id="encoding-color-field" class="input" x-model="encodings.color.field"
@change="setEncodingField('color', $event.target.value)">
<option value="">None</option> <option value="">None</option>
</select> </select>
</div> </div>
<div class="encoding-type"> <div class="encoding-type">
<label class="encoding-type-label">Type</label> <label class="encoding-type-label">Type</label>
<div class="type-toggle-group"> <div class="type-toggle-group">
<button class="btn btn-toggle small" :class="{ 'active': encodings.color.type === 'quantitative' }" @click="setEncodingType('color', 'quantitative')" data-encoding="color" data-type="quantitative" title="Quantitative">Q</button> <button class="btn btn-toggle small"
<button class="btn btn-toggle small" :class="{ 'active': encodings.color.type === 'ordinal' }" @click="setEncodingType('color', 'ordinal')" data-encoding="color" data-type="ordinal" title="Ordinal">O</button> :class="{ 'active': encodings.color.type === 'quantitative' }"
<button class="btn btn-toggle small" :class="{ 'active': encodings.color.type === 'nominal' }" @click="setEncodingType('color', 'nominal')" data-encoding="color" data-type="nominal" title="Nominal">N</button> @click="setEncodingType('color', 'quantitative')" data-encoding="color"
<button class="btn btn-toggle small" :class="{ 'active': encodings.color.type === 'temporal' }" @click="setEncodingType('color', 'temporal')" data-encoding="color" data-type="temporal" title="Temporal">T</button> data-type="quantitative" title="Quantitative">Q</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.color.type === 'ordinal' }"
@click="setEncodingType('color', 'ordinal')" data-encoding="color"
data-type="ordinal" title="Ordinal">O</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.color.type === 'nominal' }"
@click="setEncodingType('color', 'nominal')" data-encoding="color"
data-type="nominal" title="Nominal">N</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.color.type === 'temporal' }"
@click="setEncodingType('color', 'temporal')" data-encoding="color"
data-type="temporal" title="Temporal">T</button>
</div> </div>
</div> </div>
</div> </div>
@@ -563,17 +616,30 @@
<div class="encoding-group"> <div class="encoding-group">
<div class="encoding-row"> <div class="encoding-row">
<label class="encoding-header">Size</label> <label class="encoding-header">Size</label>
<select id="encoding-size-field" class="input" x-model="encodings.size.field" @change="setEncodingField('size', $event.target.value)"> <select id="encoding-size-field" class="input" x-model="encodings.size.field"
@change="setEncodingField('size', $event.target.value)">
<option value="">None</option> <option value="">None</option>
</select> </select>
</div> </div>
<div class="encoding-type"> <div class="encoding-type">
<label class="encoding-type-label">Type</label> <label class="encoding-type-label">Type</label>
<div class="type-toggle-group"> <div class="type-toggle-group">
<button class="btn btn-toggle small" :class="{ 'active': encodings.size.type === 'quantitative' }" @click="setEncodingType('size', 'quantitative')" data-encoding="size" data-type="quantitative" title="Quantitative">Q</button> <button class="btn btn-toggle small"
<button class="btn btn-toggle small" :class="{ 'active': encodings.size.type === 'ordinal' }" @click="setEncodingType('size', 'ordinal')" data-encoding="size" data-type="ordinal" title="Ordinal">O</button> :class="{ 'active': encodings.size.type === 'quantitative' }"
<button class="btn btn-toggle small" :class="{ 'active': encodings.size.type === 'nominal' }" @click="setEncodingType('size', 'nominal')" data-encoding="size" data-type="nominal" title="Nominal">N</button> @click="setEncodingType('size', 'quantitative')" data-encoding="size"
<button class="btn btn-toggle small" :class="{ 'active': encodings.size.type === 'temporal' }" @click="setEncodingType('size', 'temporal')" data-encoding="size" data-type="temporal" title="Temporal">T</button> data-type="quantitative" title="Quantitative">Q</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.size.type === 'ordinal' }"
@click="setEncodingType('size', 'ordinal')" data-encoding="size"
data-type="ordinal" title="Ordinal">O</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.size.type === 'nominal' }"
@click="setEncodingType('size', 'nominal')" data-encoding="size"
data-type="nominal" title="Nominal">N</button>
<button class="btn btn-toggle small"
:class="{ 'active': encodings.size.type === 'temporal' }"
@click="setEncodingType('size', 'temporal')" data-encoding="size"
data-type="temporal" title="Temporal">T</button>
</div> </div>
</div> </div>
</div> </div>
@@ -584,11 +650,14 @@
<div class="chart-dimensions-group"> <div class="chart-dimensions-group">
<div class="dimension-input-group"> <div class="dimension-input-group">
<label class="dimension-label">Width</label> <label class="dimension-label">Width</label>
<input type="number" id="chart-width" class="input small" x-model.number="width" @input="updatePreview()" placeholder="auto" min="1" /> <input type="number" id="chart-width" class="input small" x-model.number="width"
@input="updatePreview()" placeholder="auto" min="1" />
</div> </div>
<div class="dimension-input-group"> <div class="dimension-input-group">
<label class="dimension-label">Height</label> <label class="dimension-label">Height</label>
<input type="number" id="chart-height" class="input small" x-model.number="height" @input="updatePreview()" placeholder="auto" min="1" /> <input type="number" id="chart-height" class="input small"
x-model.number="height" @input="updatePreview()" placeholder="auto"
min="1" />
</div> </div>
</div> </div>
</div> </div>
@@ -596,8 +665,11 @@
<div class="chart-builder-error" id="chart-builder-error"></div> <div class="chart-builder-error" id="chart-builder-error"></div>
<div class="chart-builder-actions"> <div class="chart-builder-actions">
<button class="btn btn-modal primary" id="chart-builder-create-btn" @click="createSnippet()" :disabled="!isValid" title="Create snippet from chart">Create Snippet</button> <button class="btn btn-modal primary" id="chart-builder-create-btn"
<button class="btn btn-modal" id="chart-builder-cancel-btn" @click="close()" title="Cancel and close">Cancel</button> @click="createSnippet()" :disabled="!isValid"
title="Create snippet from chart">Create Snippet</button>
<button class="btn btn-modal" id="chart-builder-cancel-btn" @click="close()"
title="Cancel and close">Cancel</button>
</div> </div>
</div> </div>
@@ -630,13 +702,16 @@
<h3 class="help-heading">About Astrolabe</h3> <h3 class="help-heading">About Astrolabe</h3>
<p class="help-text"> <p class="help-text">
Astrolabe is a lightweight, browser-based snippet manager for Vega-Lite visualizations. 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 It's designed to help you quickly create, organize, and iterate on visualization specs
without
the overhead of a full development environment. the overhead of a full development environment.
</p> </p>
<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
As a Progressive Web App, Astrolabe works fully offline after your first visit and can be installed sessions.
As a Progressive Web App, Astrolabe works fully offline after your first visit and can be
installed
as a standalone application. as a standalone application.
</p> </p>
</section> </section>
@@ -645,13 +720,19 @@
<section class="help-section"> <section class="help-section">
<h3 class="help-heading">Key Features</h3> <h3 class="help-heading">Key Features</h3>
<ul class="help-list"> <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>Three-panel workspace</strong> — Snippet library, Monaco code editor with
<li><strong>Draft/published workflow</strong> — Experiment safely without losing your working version</li> Vega-Lite schema validation, and live preview</li>
<li><strong>Dataset library</strong> — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)</li> <li><strong>Draft/published workflow</strong> — Experiment safely without losing your
<li><strong>Offline-capable</strong> — Works without internet connection after first visit; install as standalone app</li> working version</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><strong>Search and sorting</strong> — Find snippets by name, comment, or spec content</li> </li>
<li><strong>Search and sorting</strong> — Find snippets by name, comment, or spec content
</li>
</ul> </ul>
</section> </section>
@@ -661,19 +742,23 @@
<div class="help-workflow"> <div class="help-workflow">
<div class="help-step"> <div class="help-step">
<strong>1. Create a snippet</strong> <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> <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>
<div class="help-step"> <div class="help-step">
<strong>2. Edit in the Draft view</strong> <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> <p>Changes auto-save as you type. The preview updates automatically. Your published
version stays safe until you're ready.</p>
</div> </div>
<div class="help-step"> <div class="help-step">
<strong>3. Publish when ready</strong> <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> <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>
<div class="help-step"> <div class="help-step">
<strong>4. Organize with datasets</strong> <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> <p>Open the Dataset Manager to create reusable datasets. Reference them in your specs
using <code>{"data": {"name": "dataset-name"}}</code>.</p>
</div> </div>
</div> </div>
</section> </section>
@@ -712,14 +797,19 @@
<h3 class="help-heading">Storage & Limits</h3> <h3 class="help-heading">Storage & Limits</h3>
<div class="help-warning"> <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, <strong>⚠️ Important:</strong> All data is stored in your browser's local storage. If you
your snippets and datasets will be permanently deleted. Regular exports are recommended as backups. clear your browser cache or site data,
your snippets and datasets will be permanently deleted. Regular exports are recommended as
backups.
</div> </div>
<ul class="help-list"> <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>Snippets</strong> — Stored in localStorage with a 5 MB limit (shared across all
<li><strong>Datasets</strong> — Stored in IndexedDB with effectively unlimited space (browser-dependent, typically 50 MB+).</li> snippets). The storage monitor shows current usage.</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> <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> </ul>
</section> </section>
@@ -727,12 +817,18 @@
<section class="help-section"> <section class="help-section">
<h3 class="help-heading">Tips & Tricks</h3> <h3 class="help-heading">Tips & Tricks</h3>
<ul class="help-list"> <ul class="help-list">
<li><strong>Sort snippets</strong> — Use the sort buttons to organize by Modified, Created, Name, or Size. Click a button twice to reverse the sort order (⬇ becomes ⬆).</li> <li><strong>Sort snippets</strong> — Use the sort buttons to organize by Modified, Created,
<li><strong>Extract inline data</strong> — When editing a spec with inline data, click "Extract to Dataset" to create a reusable dataset automatically.</li> Name, or Size. Click a button twice to reverse the sort order (⬇ becomes ⬆).</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>Extract inline data</strong>When editing a spec with inline data, click
<li><strong>Search across specs</strong> — The search box looks inside snippet names, comments, and the spec content itself.</li> "Extract to Dataset" to create a reusable dataset automatically.</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>Dataset references</strong>Astrolabe resolves dataset references at render
<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> 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> </ul>
</section> </section>
@@ -743,17 +839,28 @@
<strong>Your data stays yours.</strong> Astrolabe is built with privacy as a core principle: <strong>Your data stays yours.</strong> Astrolabe is built with privacy as a core principle:
</p> </p>
<ul class="help-list"> <ul class="help-list">
<li><strong>Local-first architecture</strong> — All snippets and datasets are stored in your browser (localStorage and IndexedDB). Nothing is sent to any server.</li> <li><strong>Local-first architecture</strong> — All snippets and datasets are stored in your
<li><strong>No accounts, no signup</strong> — There's no authentication system, no user profiles, no cloud sync. Your work exists only on your machine.</li> browser (localStorage and IndexedDB). Nothing is sent to any server.</li>
<li><strong>No cookies</strong>Astrolabe doesn't use cookies or any persistent tracking identifiers.</li> <li><strong>No accounts, no signup</strong>There's no authentication system, no user
<li><strong>Privacy-friendly analytics</strong> — We use GoatCounter (privacy-focused, GDPR-compliant) to track basic usage patterns like "snippet created" or "dataset exported." We collect <strong>zero personal information</strong>: no snippet names, no dataset content, no IP addresses, no user identifiers. Just aggregate counts to understand which features are used.</li> profiles, no cloud sync. Your work exists only on your machine.</li>
<li><strong>Data portability</strong>Export all your snippets and datasets anytime as standard JSON/CSV/TSV files. No vendor lock-in.</li> <li><strong>No cookies</strong>Astrolabe doesn't use cookies or any persistent tracking
identifiers.</li>
<li><strong>Privacy-friendly analytics</strong> — We use GoatCounter (privacy-focused,
GDPR-compliant) to track basic usage patterns like "snippet created" or "dataset
exported." We collect <strong>zero personal information</strong>: no snippet names, no
dataset content, no IP addresses, no user identifiers. Just aggregate counts to
understand which features are used.</li>
<li><strong>Data portability</strong> — Export all your snippets and datasets anytime as
standard JSON/CSV/TSV files. No vendor lock-in.</li>
</ul> </ul>
<p class="help-text"> <p class="help-text">
<strong>What analytics we collect:</strong> Action types (e.g., "snippet-create", "dataset-export"), generic metadata (e.g., format types like JSON/CSV, counts like "5 snippets"). That's it. <strong>What analytics we collect:</strong> Action types (e.g., "snippet-create",
"dataset-export"), generic metadata (e.g., format types like JSON/CSV, counts like "5
snippets"). That's it.
</p> </p>
<p class="help-text"> <p class="help-text">
<strong>What we DON'T collect:</strong> Snippet names, dataset names, actual data content, URLs, email addresses, or any personally identifiable information. <strong>What we DON'T collect:</strong> Snippet names, dataset names, actual data content,
URLs, email addresses, or any personally identifiable information.
</p> </p>
</section> </section>
</div> </div>
@@ -773,15 +880,18 @@
<section class="help-section"> <section class="help-section">
<h3 class="help-heading">Why Donate?</h3> <h3 class="help-heading">Why Donate?</h3>
<p class="help-text"> <p class="help-text">
If you are reading this, you probably found Astrolabe to be useful enough to support its creators. If you are reading this, you probably found Astrolabe to be useful enough to support its
creators.
It is a free open-source project built in Kyiv, Ukraine. It is a free open-source project built in Kyiv, Ukraine.
<br> <br>
<br> <br>
This passion project of mine is possible because my relatives, friends, and colleagues took arms and joined Armed Forces This passion project of mine is possible because my relatives, friends, and colleagues took
arms and joined Armed Forces
to defend their country and loved ones against Russian invasion. to defend their country and loved ones against Russian invasion.
<br> <br>
<br> <br>
I feel deep gratitude to them, so I will humbly ask you to redirect your donations to the foundations - you'll find the links below. I feel deep gratitude to them, so I will humbly ask you to redirect your donations to the
foundations - you'll find the links below.
<br> <br>
<br> <br>
</section> </section>
@@ -789,31 +899,39 @@
<section class="help-section"> <section class="help-section">
<h3 class="help-heading">Where to Donate</h3> <h3 class="help-heading">Where to Donate</h3>
<p class="help-text"> <p class="help-text">
<ul class="help-list"> <ul class="help-list">
<li> <li>
<strong><a href="https://bank.gov.ua/en/news/all/natsionalniy-bank-vidkriv-spetsrahunok-dlya-zboru-koshtiv-na-potrebi-armiyi"> <strong><a
National Bank of Ukraine's special account for Ukraine's Armed Forces href="https://bank.gov.ua/en/news/all/natsionalniy-bank-vidkriv-spetsrahunok-dlya-zboru-koshtiv-na-potrebi-armiyi">
</a> - Direct government channel</strong> National Bank of Ukraine's special account for Ukraine's Armed Forces
</li> </a> - Direct government channel</strong>
<li> </li>
<strong><a href="https://savelife.in.ua/en/donate-en/#donate-army-card-once"> <li>
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. <strong><a href="https://savelife.in.ua/en/donate-en/#donate-army-card-once">
</li> Come Back Alive Foundation</a></strong> - One of the biggest and most
<li> trusted. They've been around since 2014 and share <a
<strong><a href="https://macpaw.foundation/"> href="https://savelife.in.ua/en/reporting-en/">detailed reports</a> of where
MacPaw Foundation</a></strong> - Founded by MacPaw (where I work). Started in 2016, shifted focus in 2022 to support the Defence Forces. money goes.
</li> </li>
<li> <li>
<strong><a href="https://foundation.kse.ua/en/humanitarian-projects/"> <strong><a href="https://macpaw.foundation/">
KSE Foundation</a></strong>. Kyiv School of Economics (where I teach). Focuses on both education and humanitarian support for people and defenders MacPaw Foundation</a></strong> - Founded by MacPaw (where I work). Started
</li> in 2016, shifted focus in 2022 to support the Defence Forces.
<li> </li>
<strong><a href="https://standforukraine.com/"> <li>
Stand for Ukraine</a></strong> - Not a foundation, but an aggregator of reliable organizations. <strong><a href="https://foundation.kse.ua/en/humanitarian-projects/">
The list of fundraisers goes beyond military and covers recovery of veterans & victims of war, shelter for the refugees and many more. KSE Foundation</a></strong>. Kyiv School of Economics (where I teach).
</li> Focuses on both education and humanitarian support for people and defenders
</ul> </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> </p>
</section> </section>
@@ -875,8 +993,10 @@
<div class="settings-item"> <div class="settings-item">
<label class="settings-label" for="setting-font-size">Font Size</label> <label class="settings-label" for="setting-font-size">Font Size</label>
<div class="settings-control"> <div class="settings-control">
<input type="range" id="setting-font-size" min="10" max="18" step="1" class="settings-slider" x-model.number="fontSize" /> <input type="range" id="setting-font-size" min="10" max="18" step="1"
<span class="settings-value" id="setting-font-size-value" x-text="fontSize + 'px'"></span> class="settings-slider" x-model.number="fontSize" />
<span class="settings-value" id="setting-font-size-value"
x-text="fontSize + 'px'"></span>
</div> </div>
</div> </div>
@@ -904,21 +1024,24 @@
<div class="settings-item"> <div class="settings-item">
<label class="settings-label"> <label class="settings-label">
<input type="checkbox" id="setting-minimap" class="settings-checkbox" x-model="minimap" /> <input type="checkbox" id="setting-minimap" class="settings-checkbox"
x-model="minimap" />
Show minimap Show minimap
</label> </label>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<label class="settings-label"> <label class="settings-label">
<input type="checkbox" id="setting-word-wrap" class="settings-checkbox" x-model="wordWrap" /> <input type="checkbox" id="setting-word-wrap" class="settings-checkbox"
x-model="wordWrap" />
Enable word wrap Enable word wrap
</label> </label>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<label class="settings-label"> <label class="settings-label">
<input type="checkbox" id="setting-line-numbers" class="settings-checkbox" x-model="lineNumbers" /> <input type="checkbox" id="setting-line-numbers" class="settings-checkbox"
x-model="lineNumbers" />
Show line numbers Show line numbers
</label> </label>
</div> </div>
@@ -931,8 +1054,10 @@
<div class="settings-item"> <div class="settings-item">
<label class="settings-label" for="setting-render-debounce">Render Delay</label> <label class="settings-label" for="setting-render-debounce">Render Delay</label>
<div class="settings-control"> <div class="settings-control">
<input type="range" id="setting-render-debounce" min="300" max="3000" step="100" class="settings-slider" x-model.number="renderDebounce" /> <input type="range" id="setting-render-debounce" min="300" max="3000" step="100"
<span class="settings-value" id="setting-render-debounce-value" x-text="renderDebounce + 'ms'"></span> class="settings-slider" x-model.number="renderDebounce" />
<span class="settings-value" id="setting-render-debounce-value"
x-text="renderDebounce + 'ms'"></span>
</div> </div>
<div class="settings-hint">Delay before visualization updates while typing</div> <div class="settings-hint">Delay before visualization updates while typing</div>
</div> </div>
@@ -957,29 +1082,24 @@
<div class="settings-item" id="custom-date-format-item" x-show="showCustomDateFormat"> <div class="settings-item" id="custom-date-format-item" x-show="showCustomDateFormat">
<label class="settings-label" for="setting-custom-date-format">Custom Format</label> <label class="settings-label" for="setting-custom-date-format">Custom Format</label>
<div class="settings-control"> <div class="settings-control">
<input type="text" id="setting-custom-date-format" class="settings-input" placeholder="yyyy-MM-dd HH:mm" x-model="customDateFormat" /> <input type="text" id="setting-custom-date-format" class="settings-input"
placeholder="yyyy-MM-dd HH:mm" x-model="customDateFormat" />
</div> </div>
<div class="settings-hint"> <div class="settings-hint">
Tokens: yyyy (year), MM (month), dd (day), HH (24h), hh (12h), mm (min), ss (sec), a/A (am/pm) Tokens: yyyy (year), MM (month), dd (day), HH (24h), hh (12h), mm (min), ss (sec), a/A
(am/pm)
</div> </div>
</div> </div>
</section> </section>
<!-- Actions --> <!-- Actions -->
<div class="settings-actions"> <div class="settings-actions">
<button class="btn btn-modal primary" <button class="btn btn-modal primary" id="settings-apply-btn" @click="apply()"
id="settings-apply-btn" :disabled="!isDirty" title="Apply and save settings">Apply</button>
@click="apply()" <button class="btn btn-modal" id="settings-reset-btn" @click="reset()"
:disabled="!isDirty" title="Reset to default settings">Reset to Defaults</button>
title="Apply and save settings">Apply</button> <button class="btn btn-modal" id="settings-cancel-btn" @click="cancel()"
<button class="btn btn-modal" title="Cancel without saving">Cancel</button>
id="settings-reset-btn"
@click="reset()"
title="Reset to default settings">Reset to Defaults</button>
<button class="btn btn-modal"
id="settings-cancel-btn"
@click="cancel()"
title="Cancel without saving">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
@@ -998,8 +1118,8 @@
</div> </div>
<script src="src/js/user-settings.js"></script> <script src="src/js/user-settings.js"></script>
<script src="src/js/config.js"></script>
<script src="src/js/generic-storage-ui.js"></script> <script src="src/js/generic-storage-ui.js"></script>
<script src="src/js/config.js"></script>
<script src="src/js/snippet-manager.js"></script> <script src="src/js/snippet-manager.js"></script>
<script src="src/js/dataset-manager.js"></script> <script src="src/js/dataset-manager.js"></script>
<script src="src/js/chart-builder.js"></script> <script src="src/js/chart-builder.js"></script>
@@ -1008,8 +1128,7 @@
<script src="src/js/app.js"></script> <script src="src/js/app.js"></script>
<!-- GoatCounter Analytics --> <!-- GoatCounter Analytics -->
<script data-goatcounter="https://astrolabe.goatcounter.com/count" <script data-goatcounter="https://astrolabe.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
async src="//gc.zgo.at/count.js"></script>
</body> </body>
</html> </html>

View File

@@ -303,20 +303,6 @@ document.addEventListener('DOMContentLoaded', function () {
} }
}); });
// View mode toggle buttons (now handled by Alpine.js in index.html)
// Preview fit mode buttons
document.getElementById('preview-fit-default').addEventListener('click', () => {
setPreviewFitMode('default');
});
document.getElementById('preview-fit-width').addEventListener('click', () => {
setPreviewFitMode('width');
});
document.getElementById('preview-fit-full').addEventListener('click', () => {
setPreviewFitMode('full');
});
// Publish and Revert buttons // Publish and Revert buttons
document.getElementById('publish-btn').addEventListener('click', publishDraft); document.getElementById('publish-btn').addEventListener('click', publishDraft);

View File

@@ -187,6 +187,11 @@ document.addEventListener('alpine:init', () => {
return icons[type] || icons.info; return icons[type] || icons.info;
} }
}); });
// Preview panel fit mode store
Alpine.store('preview', {
fitMode: 'default' // 'default' | 'width' | 'full'
});
}); });
// Toast Notification System (now backed by Alpine store) // Toast Notification System (now backed by Alpine store)
@@ -327,4 +332,3 @@ const sampleSpec = {
"y": { "field": "value", "type": "quantitative" } "y": { "field": "value", "type": "quantitative" }
} }
}; };

View File

@@ -194,15 +194,14 @@ function updateRenderDebounce(newDebounce) {
function setPreviewFitMode(mode) { function setPreviewFitMode(mode) {
previewFitMode = mode; previewFitMode = mode;
// Update button states
document.getElementById('preview-fit-default').classList.toggle('active', mode === 'default'); // Sync with Alpine store if available
document.getElementById('preview-fit-width').classList.toggle('active', mode === 'width'); if (typeof Alpine !== 'undefined' && Alpine.store('preview')) {
document.getElementById('preview-fit-full').classList.toggle('active', mode === 'full'); Alpine.store('preview').fitMode = mode;
}
// Save to settings // Save to settings
if (typeof updateSetting === 'function') { updateSetting('ui.previewFitMode', mode);
updateSetting('ui.previewFitMode', mode);
}
// Re-render with new fit mode // Re-render with new fit mode
renderVisualization(); renderVisualization();