mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
feat: implement user settings modal with customizable editor preferences and performance settings
This commit is contained in:
123
index.html
123
index.html
@@ -29,6 +29,7 @@
|
|||||||
<span class="header-link" id="import-link" title="Import snippets from JSON file">Import</span>
|
<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="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="datasets-link" title="Open dataset manager (Cmd/Ctrl+K)">Datasets</span>
|
||||||
|
<span class="header-link" id="settings-link" title="Open settings (Cmd/Ctrl+,)">Settings</span>
|
||||||
<span class="header-link" id="help-link" title="View keyboard shortcuts and help">Help</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>
|
<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;" />
|
<input type="file" id="import-file-input" accept=".json" style="display: none;" />
|
||||||
@@ -401,6 +402,10 @@
|
|||||||
<td class="shortcut-key">Cmd/Ctrl+K</td>
|
<td class="shortcut-key">Cmd/Ctrl+K</td>
|
||||||
<td class="shortcut-desc">Toggle dataset manager</td>
|
<td class="shortcut-desc">Toggle dataset manager</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="shortcut-key">Cmd/Ctrl+,</td>
|
||||||
|
<td class="shortcut-desc">Open settings</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="shortcut-key">Cmd/Ctrl+S</td>
|
<td class="shortcut-key">Cmd/Ctrl+S</td>
|
||||||
<td class="shortcut-desc">Publish draft (save)</td>
|
<td class="shortcut-desc">Publish draft (save)</td>
|
||||||
@@ -553,9 +558,127 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
<div id="settings-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content" style="max-width: 600px; height: auto; max-height: 85vh;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title">Settings</span>
|
||||||
|
<button class="btn btn-icon" id="settings-modal-close" title="Close settings (Escape)">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="settings-content">
|
||||||
|
<!-- Editor Settings -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h3 class="settings-heading">Editor</h3>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label" for="setting-font-size">Font Size</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<input type="range" id="setting-font-size" min="10" max="18" step="1" value="12" class="settings-slider" />
|
||||||
|
<span class="settings-value" id="setting-font-size-value">12px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label" for="setting-theme">Theme</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<select id="setting-theme" class="settings-select">
|
||||||
|
<option value="vs-light">Light</option>
|
||||||
|
<option value="vs-dark">Dark</option>
|
||||||
|
<option value="hc-black">High Contrast</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label" for="setting-tab-size">Tab Size</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<select id="setting-tab-size" class="settings-select">
|
||||||
|
<option value="2">2 spaces</option>
|
||||||
|
<option value="4">4 spaces</option>
|
||||||
|
<option value="8">8 spaces</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label">
|
||||||
|
<input type="checkbox" id="setting-minimap" class="settings-checkbox" />
|
||||||
|
Show minimap
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label">
|
||||||
|
<input type="checkbox" id="setting-word-wrap" class="settings-checkbox" checked />
|
||||||
|
Enable word wrap
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label">
|
||||||
|
<input type="checkbox" id="setting-line-numbers" class="settings-checkbox" checked />
|
||||||
|
Show line numbers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Performance Settings -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h3 class="settings-heading">Performance</h3>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label" for="setting-render-debounce">Render Delay</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<input type="range" id="setting-render-debounce" min="300" max="3000" step="100" value="1500" class="settings-slider" />
|
||||||
|
<span class="settings-value" id="setting-render-debounce-value">1500ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-hint">Delay before visualization updates while typing</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Formatting Settings -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h3 class="settings-heading">Formatting</h3>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="settings-label" for="setting-date-format">Date Format</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<select id="setting-date-format" class="settings-select">
|
||||||
|
<option value="smart">Smart (relative times)</option>
|
||||||
|
<option value="locale">Locale (browser default)</option>
|
||||||
|
<option value="iso">ISO 8601</option>
|
||||||
|
<option value="custom">Custom</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item" id="custom-date-format-item" style="display: none;">
|
||||||
|
<label class="settings-label" for="setting-custom-date-format">Custom Format</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<input type="text" id="setting-custom-date-format" class="settings-input" value="yyyy-MM-dd HH:mm" placeholder="yyyy-MM-dd HH:mm" />
|
||||||
|
</div>
|
||||||
|
<div class="settings-hint">
|
||||||
|
Tokens: yyyy (year), MM (month), dd (day), HH (24h), hh (12h), mm (min), ss (sec), a/A (am/pm)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="settings-actions">
|
||||||
|
<button class="btn btn-modal primary" id="settings-apply-btn" title="Apply and save settings">Apply</button>
|
||||||
|
<button class="btn btn-modal" id="settings-reset-btn" title="Reset to default settings">Reset to Defaults</button>
|
||||||
|
<button class="btn btn-modal" id="settings-cancel-btn" title="Cancel without saving">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Toast Notification Container -->
|
<!-- Toast Notification Container -->
|
||||||
<div id="toast-container"></div>
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
|
<script src="src/js/user-settings.js"></script>
|
||||||
<script src="src/js/config.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>
|
||||||
|
|||||||
266
src/js/app.js
266
src/js/app.js
@@ -1,6 +1,9 @@
|
|||||||
// Application initialization and event handlers
|
// Application initialization and event handlers
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Initialize user settings
|
||||||
|
initSettings();
|
||||||
|
|
||||||
// Initialize snippet storage and render list
|
// Initialize snippet storage and render list
|
||||||
initializeSnippetsStorage();
|
initializeSnippetsStorage();
|
||||||
|
|
||||||
@@ -59,16 +62,28 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
// Load Vega libraries before creating editor
|
// Load Vega libraries before creating editor
|
||||||
await loadVegaLibraries();
|
await loadVegaLibraries();
|
||||||
|
|
||||||
|
// Get user settings for editor configuration
|
||||||
|
const editorSettings = getSetting('editor') || {
|
||||||
|
fontSize: 12,
|
||||||
|
theme: 'vs-light',
|
||||||
|
minimap: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
lineNumbers: 'on',
|
||||||
|
tabSize: 2
|
||||||
|
};
|
||||||
|
|
||||||
// Create the editor
|
// Create the editor
|
||||||
editor = monaco.editor.create(document.getElementById('monaco-editor'), {
|
editor = monaco.editor.create(document.getElementById('monaco-editor'), {
|
||||||
value: JSON.stringify(sampleSpec, null, 2),
|
value: JSON.stringify(sampleSpec, null, 2),
|
||||||
language: 'json',
|
language: 'json',
|
||||||
theme: 'vs-light',
|
theme: editorSettings.theme,
|
||||||
fontSize: 12,
|
fontSize: editorSettings.fontSize,
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: editorSettings.minimap },
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
wordWrap: 'on',
|
wordWrap: editorSettings.wordWrap,
|
||||||
|
lineNumbers: editorSettings.lineNumbers,
|
||||||
|
tabSize: editorSettings.tabSize,
|
||||||
formatOnPaste: true,
|
formatOnPaste: true,
|
||||||
formatOnType: true
|
formatOnType: true
|
||||||
});
|
});
|
||||||
@@ -135,6 +150,80 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings Modal
|
||||||
|
const settingsLink = document.getElementById('settings-link');
|
||||||
|
const settingsModal = document.getElementById('settings-modal');
|
||||||
|
const settingsModalClose = document.getElementById('settings-modal-close');
|
||||||
|
const settingsApplyBtn = document.getElementById('settings-apply-btn');
|
||||||
|
const settingsResetBtn = document.getElementById('settings-reset-btn');
|
||||||
|
const settingsCancelBtn = document.getElementById('settings-cancel-btn');
|
||||||
|
|
||||||
|
if (settingsLink) {
|
||||||
|
settingsLink.addEventListener('click', function () {
|
||||||
|
openSettingsModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsModalClose) {
|
||||||
|
settingsModalClose.addEventListener('click', closeSettingsModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsCancelBtn) {
|
||||||
|
settingsCancelBtn.addEventListener('click', closeSettingsModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsApplyBtn) {
|
||||||
|
settingsApplyBtn.addEventListener('click', applySettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsResetBtn) {
|
||||||
|
settingsResetBtn.addEventListener('click', function() {
|
||||||
|
if (confirm('Reset all settings to defaults? This cannot be undone.')) {
|
||||||
|
resetSettings();
|
||||||
|
loadSettingsIntoUI();
|
||||||
|
Toast.show('Settings reset to defaults', 'success');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close on overlay click
|
||||||
|
if (settingsModal) {
|
||||||
|
settingsModal.addEventListener('click', function (e) {
|
||||||
|
if (e.target === settingsModal) {
|
||||||
|
closeSettingsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings UI interactions
|
||||||
|
const fontSizeSlider = document.getElementById('setting-font-size');
|
||||||
|
const fontSizeValue = document.getElementById('setting-font-size-value');
|
||||||
|
if (fontSizeSlider && fontSizeValue) {
|
||||||
|
fontSizeSlider.addEventListener('input', function() {
|
||||||
|
fontSizeValue.textContent = this.value + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderDebounceSlider = document.getElementById('setting-render-debounce');
|
||||||
|
const renderDebounceValue = document.getElementById('setting-render-debounce-value');
|
||||||
|
if (renderDebounceSlider && renderDebounceValue) {
|
||||||
|
renderDebounceSlider.addEventListener('input', function() {
|
||||||
|
renderDebounceValue.textContent = this.value + 'ms';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateFormatSelect = document.getElementById('setting-date-format');
|
||||||
|
const customDateFormatItem = document.getElementById('custom-date-format-item');
|
||||||
|
if (dateFormatSelect && customDateFormatItem) {
|
||||||
|
dateFormatSelect.addEventListener('change', function() {
|
||||||
|
if (this.value === 'custom') {
|
||||||
|
customDateFormatItem.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
customDateFormatItem.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Dataset Manager
|
// Dataset Manager
|
||||||
const datasetsLink = document.getElementById('datasets-link');
|
const datasetsLink = document.getElementById('datasets-link');
|
||||||
const toggleDatasetsBtn = document.getElementById('toggle-datasets');
|
const toggleDatasetsBtn = document.getElementById('toggle-datasets');
|
||||||
@@ -384,11 +473,21 @@ const KeyboardActions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleSettings: function() {
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
if (modal && modal.style.display === 'flex') {
|
||||||
|
closeSettingsModal();
|
||||||
|
} else {
|
||||||
|
openSettingsModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
closeAnyModal: function() {
|
closeAnyModal: function() {
|
||||||
const helpModal = document.getElementById('help-modal');
|
const helpModal = document.getElementById('help-modal');
|
||||||
const datasetModal = document.getElementById('dataset-modal');
|
const datasetModal = document.getElementById('dataset-modal');
|
||||||
const extractModal = document.getElementById('extract-modal');
|
const extractModal = document.getElementById('extract-modal');
|
||||||
const donateModal = document.getElementById('donate-modal');
|
const donateModal = document.getElementById('donate-modal');
|
||||||
|
const settingsModal = document.getElementById('settings-modal');
|
||||||
|
|
||||||
if (helpModal && helpModal.style.display === 'flex') {
|
if (helpModal && helpModal.style.display === 'flex') {
|
||||||
closeHelpModal();
|
closeHelpModal();
|
||||||
@@ -406,6 +505,10 @@ const KeyboardActions = {
|
|||||||
closeDonateModal();
|
closeDonateModal();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (settingsModal && settingsModal.style.display === 'flex') {
|
||||||
|
closeSettingsModal();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -437,6 +540,13 @@ function initializeKeyboardShortcuts() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cmd/Ctrl+,: Toggle settings
|
||||||
|
if (modifierKey && e.key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
KeyboardActions.toggleSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Cmd/Ctrl+S: Publish draft
|
// Cmd/Ctrl+S: Publish draft
|
||||||
if (modifierKey && e.key.toLowerCase() === 's') {
|
if (modifierKey && e.key.toLowerCase() === 's') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -496,3 +606,151 @@ function closeDonateModal() {
|
|||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings modal functions
|
||||||
|
function openSettingsModal() {
|
||||||
|
loadSettingsIntoUI();
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
// Track event
|
||||||
|
Analytics.track('modal-settings', 'Open Settings modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettingsModal() {
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettingsIntoUI() {
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
// Editor settings
|
||||||
|
const fontSizeSlider = document.getElementById('setting-font-size');
|
||||||
|
const fontSizeValue = document.getElementById('setting-font-size-value');
|
||||||
|
if (fontSizeSlider && fontSizeValue) {
|
||||||
|
fontSizeSlider.value = settings.editor.fontSize;
|
||||||
|
fontSizeValue.textContent = settings.editor.fontSize + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSelect = document.getElementById('setting-theme');
|
||||||
|
if (themeSelect) {
|
||||||
|
themeSelect.value = settings.editor.theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabSizeSelect = document.getElementById('setting-tab-size');
|
||||||
|
if (tabSizeSelect) {
|
||||||
|
tabSizeSelect.value = settings.editor.tabSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimapCheckbox = document.getElementById('setting-minimap');
|
||||||
|
if (minimapCheckbox) {
|
||||||
|
minimapCheckbox.checked = settings.editor.minimap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wordWrapCheckbox = document.getElementById('setting-word-wrap');
|
||||||
|
if (wordWrapCheckbox) {
|
||||||
|
wordWrapCheckbox.checked = settings.editor.wordWrap === 'on';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineNumbersCheckbox = document.getElementById('setting-line-numbers');
|
||||||
|
if (lineNumbersCheckbox) {
|
||||||
|
lineNumbersCheckbox.checked = settings.editor.lineNumbers === 'on';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance settings
|
||||||
|
const renderDebounceSlider = document.getElementById('setting-render-debounce');
|
||||||
|
const renderDebounceValue = document.getElementById('setting-render-debounce-value');
|
||||||
|
if (renderDebounceSlider && renderDebounceValue) {
|
||||||
|
renderDebounceSlider.value = settings.performance.renderDebounce;
|
||||||
|
renderDebounceValue.textContent = settings.performance.renderDebounce + 'ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatting settings
|
||||||
|
const dateFormatSelect = document.getElementById('setting-date-format');
|
||||||
|
const customDateFormatItem = document.getElementById('custom-date-format-item');
|
||||||
|
if (dateFormatSelect) {
|
||||||
|
dateFormatSelect.value = settings.formatting.dateFormat;
|
||||||
|
if (customDateFormatItem) {
|
||||||
|
customDateFormatItem.style.display = settings.formatting.dateFormat === 'custom' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customDateFormatInput = document.getElementById('setting-custom-date-format');
|
||||||
|
if (customDateFormatInput) {
|
||||||
|
customDateFormatInput.value = settings.formatting.customDateFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySettings() {
|
||||||
|
// Collect values from UI
|
||||||
|
const newSettings = {
|
||||||
|
'editor.fontSize': parseInt(document.getElementById('setting-font-size').value),
|
||||||
|
'editor.theme': document.getElementById('setting-theme').value,
|
||||||
|
'editor.tabSize': parseInt(document.getElementById('setting-tab-size').value),
|
||||||
|
'editor.minimap': document.getElementById('setting-minimap').checked,
|
||||||
|
'editor.wordWrap': document.getElementById('setting-word-wrap').checked ? 'on' : 'off',
|
||||||
|
'editor.lineNumbers': document.getElementById('setting-line-numbers').checked ? 'on' : 'off',
|
||||||
|
'performance.renderDebounce': parseInt(document.getElementById('setting-render-debounce').value),
|
||||||
|
'formatting.dateFormat': document.getElementById('setting-date-format').value,
|
||||||
|
'formatting.customDateFormat': document.getElementById('setting-custom-date-format').value
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate settings
|
||||||
|
let hasErrors = false;
|
||||||
|
for (const [path, value] of Object.entries(newSettings)) {
|
||||||
|
const errors = validateSetting(path, value);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
Toast.show(errors.join(', '), 'error');
|
||||||
|
hasErrors = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
if (updateSettings(newSettings)) {
|
||||||
|
// Apply editor settings immediately
|
||||||
|
if (editor) {
|
||||||
|
editor.updateOptions({
|
||||||
|
fontSize: newSettings['editor.fontSize'],
|
||||||
|
theme: newSettings['editor.theme'],
|
||||||
|
tabSize: newSettings['editor.tabSize'],
|
||||||
|
minimap: { enabled: newSettings['editor.minimap'] },
|
||||||
|
wordWrap: newSettings['editor.wordWrap'],
|
||||||
|
lineNumbers: newSettings['editor.lineNumbers']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update debounced render function
|
||||||
|
if (typeof updateRenderDebounce === 'function') {
|
||||||
|
updateRenderDebounce(newSettings['performance.renderDebounce']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render snippet list to reflect date format changes
|
||||||
|
renderSnippetList();
|
||||||
|
|
||||||
|
// Update metadata display if a snippet is selected
|
||||||
|
if (window.currentSnippetId) {
|
||||||
|
const snippet = SnippetStorage.getSnippet(window.currentSnippetId);
|
||||||
|
if (snippet) {
|
||||||
|
document.getElementById('snippet-created').textContent = formatDate(snippet.created, true);
|
||||||
|
document.getElementById('snippet-modified').textContent = formatDate(snippet.modified, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.show('Settings applied successfully', 'success');
|
||||||
|
closeSettingsModal();
|
||||||
|
|
||||||
|
// Track event
|
||||||
|
Analytics.track('settings-apply', 'Applied settings');
|
||||||
|
} else {
|
||||||
|
Toast.show('Failed to save settings', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -111,7 +111,14 @@ function debouncedRender() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(renderTimeout);
|
clearTimeout(renderTimeout);
|
||||||
renderTimeout = setTimeout(renderVisualization, 1500);
|
const debounceTime = getSetting('performance.renderDebounce') || 1500;
|
||||||
|
renderTimeout = setTimeout(renderVisualization, debounceTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update render debounce setting (called when settings are changed)
|
||||||
|
function updateRenderDebounce(newDebounce) {
|
||||||
|
// The next render will automatically use the new debounce time
|
||||||
|
// No need to do anything special here
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Vega libraries dynamically with UMD builds
|
// Load Vega libraries dynamically with UMD builds
|
||||||
|
|||||||
@@ -281,28 +281,14 @@ function initializeSnippetsStorage() {
|
|||||||
return existingSnippets;
|
return existingSnippets;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format date for display in snippet list
|
// Format date for display in snippet list (delegates to user-settings.js)
|
||||||
function formatSnippetDate(isoString) {
|
function formatSnippetDate(isoString) {
|
||||||
const date = new Date(isoString);
|
return formatDate(isoString, false);
|
||||||
const diffDays = Math.floor((new Date() - date) / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
if (diffDays === 0) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
||||||
if (diffDays === 1) return 'Yesterday';
|
|
||||||
if (diffDays < 7) return `${diffDays} days ago`;
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format full date/time for display in meta info
|
// Format full date/time for display in meta info (delegates to user-settings.js)
|
||||||
function formatFullDate(isoString) {
|
function formatFullDate(isoString) {
|
||||||
const date = new Date(isoString);
|
return formatDate(isoString, true);
|
||||||
return date.toLocaleString([], {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render snippet list in the UI
|
// Render snippet list in the UI
|
||||||
|
|||||||
288
src/js/user-settings.js
Normal file
288
src/js/user-settings.js
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
// user-settings.js - User preferences and configuration management
|
||||||
|
|
||||||
|
const SETTINGS_STORAGE_KEY = 'astrolabe-user-settings';
|
||||||
|
|
||||||
|
// Default settings configuration
|
||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
version: 1,
|
||||||
|
|
||||||
|
editor: {
|
||||||
|
fontSize: 12, // 10-18px
|
||||||
|
theme: 'vs-light', // 'vs-light' | 'vs-dark' | 'hc-black'
|
||||||
|
minimap: false, // true | false
|
||||||
|
wordWrap: 'on', // 'on' | 'off'
|
||||||
|
lineNumbers: 'on', // 'on' | 'off'
|
||||||
|
tabSize: 2 // 2-8 spaces
|
||||||
|
},
|
||||||
|
|
||||||
|
performance: {
|
||||||
|
renderDebounce: 1500 // 300-3000ms - delay before visualization renders
|
||||||
|
},
|
||||||
|
|
||||||
|
formatting: {
|
||||||
|
dateFormat: 'smart', // 'smart' | 'locale' | 'iso' | 'custom'
|
||||||
|
customDateFormat: 'yyyy-MM-dd HH:mm' // Used when dateFormat === 'custom'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current user settings (loaded from localStorage or defaults)
|
||||||
|
let userSettings = null;
|
||||||
|
|
||||||
|
// Initialize settings - load from localStorage or use defaults
|
||||||
|
function initSettings() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
// Merge with defaults to handle version upgrades
|
||||||
|
userSettings = mergeWithDefaults(parsed);
|
||||||
|
} else {
|
||||||
|
userSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading user settings:', error);
|
||||||
|
userSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
}
|
||||||
|
return userSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge stored settings with defaults (handles new settings in updates)
|
||||||
|
function mergeWithDefaults(stored) {
|
||||||
|
const merged = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
|
||||||
|
// Merge each section
|
||||||
|
for (const section in stored) {
|
||||||
|
if (merged[section] && typeof merged[section] === 'object') {
|
||||||
|
Object.assign(merged[section], stored[section]);
|
||||||
|
} else {
|
||||||
|
merged[section] = stored[section];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings to localStorage
|
||||||
|
function saveSettings() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(userSettings));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving user settings:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all current settings
|
||||||
|
function getSettings() {
|
||||||
|
if (!userSettings) {
|
||||||
|
initSettings();
|
||||||
|
}
|
||||||
|
return userSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a specific setting by path (e.g., 'editor.fontSize')
|
||||||
|
function getSetting(path) {
|
||||||
|
const settings = getSettings();
|
||||||
|
const parts = path.split('.');
|
||||||
|
let value = settings;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (value && typeof value === 'object' && part in value) {
|
||||||
|
value = value[part];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a specific setting by path
|
||||||
|
function updateSetting(path, value) {
|
||||||
|
const settings = getSettings();
|
||||||
|
const parts = path.split('.');
|
||||||
|
let target = settings;
|
||||||
|
|
||||||
|
// Navigate to the parent object
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (!target[part] || typeof target[part] !== 'object') {
|
||||||
|
target[part] = {};
|
||||||
|
}
|
||||||
|
target = target[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the value
|
||||||
|
const lastPart = parts[parts.length - 1];
|
||||||
|
target[lastPart] = value;
|
||||||
|
|
||||||
|
return saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update multiple settings at once
|
||||||
|
function updateSettings(updates) {
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
for (const path in updates) {
|
||||||
|
const parts = path.split('.');
|
||||||
|
let target = settings;
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (!target[part] || typeof target[part] !== 'object') {
|
||||||
|
target[part] = {};
|
||||||
|
}
|
||||||
|
target = target[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPart = parts[parts.length - 1];
|
||||||
|
target[lastPart] = updates[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all settings to defaults
|
||||||
|
function resetSettings() {
|
||||||
|
userSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
return saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export settings as JSON
|
||||||
|
function exportSettings() {
|
||||||
|
return JSON.stringify(getSettings(), null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import settings from JSON string
|
||||||
|
function importSettings(jsonString) {
|
||||||
|
try {
|
||||||
|
const imported = JSON.parse(jsonString);
|
||||||
|
userSettings = mergeWithDefaults(imported);
|
||||||
|
return saveSettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing settings:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date according to user settings
|
||||||
|
function formatDate(isoString, useFullFormat = false) {
|
||||||
|
const date = new Date(isoString);
|
||||||
|
const format = getSetting('formatting.dateFormat');
|
||||||
|
|
||||||
|
if (!useFullFormat && format === 'smart') {
|
||||||
|
// Smart format: relative for recent dates
|
||||||
|
const diffMs = new Date() - date;
|
||||||
|
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Just now';
|
||||||
|
if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
|
||||||
|
if (diffDays === 0) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
if (diffDays === 1) return 'Yesterday';
|
||||||
|
if (diffDays < 7) return `${diffDays} days ago`;
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'locale') {
|
||||||
|
return useFullFormat
|
||||||
|
? date.toLocaleString([], {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
: date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'iso') {
|
||||||
|
return useFullFormat
|
||||||
|
? date.toISOString()
|
||||||
|
: date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'custom') {
|
||||||
|
const customFormat = getSetting('formatting.customDateFormat');
|
||||||
|
return formatCustomDate(date, customFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to locale
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date using custom format tokens
|
||||||
|
function formatCustomDate(date, format) {
|
||||||
|
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
||||||
|
|
||||||
|
const tokens = {
|
||||||
|
'yyyy': date.getFullYear(),
|
||||||
|
'yy': String(date.getFullYear()).slice(-2),
|
||||||
|
'MM': pad(date.getMonth() + 1),
|
||||||
|
'M': date.getMonth() + 1,
|
||||||
|
'dd': pad(date.getDate()),
|
||||||
|
'd': date.getDate(),
|
||||||
|
'HH': pad(date.getHours()),
|
||||||
|
'H': date.getHours(),
|
||||||
|
'hh': pad(date.getHours() % 12 || 12),
|
||||||
|
'h': date.getHours() % 12 || 12,
|
||||||
|
'mm': pad(date.getMinutes()),
|
||||||
|
'm': date.getMinutes(),
|
||||||
|
'ss': pad(date.getSeconds()),
|
||||||
|
's': date.getSeconds(),
|
||||||
|
'a': date.getHours() < 12 ? 'am' : 'pm',
|
||||||
|
'A': date.getHours() < 12 ? 'AM' : 'PM'
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = format;
|
||||||
|
// Sort by length descending to replace longer tokens first
|
||||||
|
const sortedTokens = Object.keys(tokens).sort((a, b) => b.length - a.length);
|
||||||
|
for (const token of sortedTokens) {
|
||||||
|
result = result.replace(new RegExp(token, 'g'), tokens[token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate setting value
|
||||||
|
function validateSetting(path, value) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (path === 'editor.fontSize') {
|
||||||
|
if (typeof value !== 'number' || value < 10 || value > 18) {
|
||||||
|
errors.push('Font size must be between 10 and 18');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === 'editor.theme') {
|
||||||
|
const validThemes = ['vs-light', 'vs-dark', 'hc-black'];
|
||||||
|
if (!validThemes.includes(value)) {
|
||||||
|
errors.push('Invalid theme value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === 'editor.tabSize') {
|
||||||
|
if (typeof value !== 'number' || value < 2 || value > 8) {
|
||||||
|
errors.push('Tab size must be between 2 and 8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === 'performance.renderDebounce') {
|
||||||
|
if (typeof value !== 'number' || value < 300 || value > 3000) {
|
||||||
|
errors.push('Render debounce must be between 300 and 3000ms');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === 'formatting.dateFormat') {
|
||||||
|
const validFormats = ['smart', 'locale', 'iso', 'custom'];
|
||||||
|
if (!validFormats.includes(value)) {
|
||||||
|
errors.push('Invalid date format value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
@@ -327,6 +327,39 @@ body { font-family: var(--font-main); height: 100vh; overflow: hidden; backgroun
|
|||||||
.donate-two-column { display: flex; gap: 20px; margin-bottom: 24px; }
|
.donate-two-column { display: flex; gap: 20px; margin-bottom: 24px; }
|
||||||
.donate-two-column .help-section { flex: 1; margin-bottom: 0; }
|
.donate-two-column .help-section { flex: 1; margin-bottom: 0; }
|
||||||
|
|
||||||
|
/* Settings Modal */
|
||||||
|
.settings-content { padding: 20px; }
|
||||||
|
.settings-section { margin-bottom: 24px; padding-bottom: 24px; border-bottom: 1px solid var(--win-gray-dark); }
|
||||||
|
.settings-section:last-of-type { border-bottom: none; padding-bottom: 0; }
|
||||||
|
.settings-heading { font-size: 14px; font-weight: bold; margin: 0 0 16px 0; color: var(--win-blue-dark); }
|
||||||
|
|
||||||
|
.settings-item { margin-bottom: 16px; }
|
||||||
|
.settings-item:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.settings-label { display: block; font-size: 12px; font-weight: 500; color: #333; margin-bottom: 6px; }
|
||||||
|
.settings-label input[type="checkbox"] { margin-right: 6px; vertical-align: middle; }
|
||||||
|
|
||||||
|
.settings-control { display: flex; align-items: center; gap: 12px; }
|
||||||
|
|
||||||
|
.settings-slider { flex: 1; height: 4px; background: var(--win-gray-dark); outline: none; border-radius: 2px; -webkit-appearance: none; }
|
||||||
|
.settings-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: var(--win-blue); border: 2px solid var(--win-blue-dark); border-radius: 50%; cursor: pointer; }
|
||||||
|
.settings-slider::-moz-range-thumb { width: 16px; height: 16px; background: var(--win-blue); border: 2px solid var(--win-blue-dark); border-radius: 50%; cursor: pointer; }
|
||||||
|
|
||||||
|
.settings-value { font-size: 12px; font-family: var(--font-mono); color: var(--win-gray-darker); min-width: 60px; text-align: right; }
|
||||||
|
|
||||||
|
.settings-select { flex: 1; padding: 4px 8px; font-size: 12px; font-family: var(--font-mono); background: var(--bg-white); border: 2px inset var(--win-gray); color: #000; }
|
||||||
|
.settings-select:focus { outline: 1px dotted #000; }
|
||||||
|
|
||||||
|
.settings-input { width: 100%; padding: 4px 8px; font-size: 12px; font-family: var(--font-mono); background: var(--bg-white); border: 2px inset var(--win-gray); color: #000; }
|
||||||
|
.settings-input:focus { outline: 1px dotted #000; }
|
||||||
|
|
||||||
|
.settings-checkbox { width: 14px; height: 14px; cursor: pointer; }
|
||||||
|
|
||||||
|
.settings-hint { font-size: 11px; color: var(--win-gray-darker); margin-top: 4px; line-height: 1.4; }
|
||||||
|
|
||||||
|
.settings-actions { display: flex; gap: 8px; margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--win-gray-dark); }
|
||||||
|
.settings-actions .btn-modal { flex: 1; }
|
||||||
|
|
||||||
/* Toast Notifications */
|
/* Toast Notifications */
|
||||||
#toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 2000; display: flex; flex-direction: column-reverse; gap: 10px; max-width: 400px; }
|
#toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 2000; display: flex; flex-direction: column-reverse; gap: 10px; max-width: 400px; }
|
||||||
.toast { display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: var(--win-gray); border: 2px outset var(--win-gray); box-shadow: 4px 4px 8px rgba(0,0,0,0.3); font-size: 12px; min-width: 300px; opacity: 0; transform: translateX(400px); transition: all 0.3s ease; }
|
.toast { display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: var(--win-gray); border: 2px outset var(--win-gray); box-shadow: 4px 4px 8px rgba(0,0,0,0.3); font-size: 12px; min-width: 300px; opacity: 0; transform: translateX(400px); transition: all 0.3s ease; }
|
||||||
|
|||||||
Reference in New Issue
Block a user