mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
feat: implement Alpine.js settings panel for user preferences and configuration management
This commit is contained in:
44
index.html
44
index.html
@@ -823,7 +823,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<div id="settings-modal" class="modal" style="display: none;">
|
<div id="settings-modal" class="modal" style="display: none;" x-data="settingsPanel()">
|
||||||
<div class="modal-content" style="max-width: 600px; height: auto; max-height: 85vh;">
|
<div class="modal-content" style="max-width: 600px; height: auto; max-height: 85vh;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span class="modal-title">Settings</span>
|
<span class="modal-title">Settings</span>
|
||||||
@@ -838,7 +838,7 @@
|
|||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<label class="settings-label" for="setting-ui-theme">Theme</label>
|
<label class="settings-label" for="setting-ui-theme">Theme</label>
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<select id="setting-ui-theme" class="settings-select">
|
<select id="setting-ui-theme" class="settings-select" x-model="uiTheme">
|
||||||
<option value="light">Light</option>
|
<option value="light">Light</option>
|
||||||
<option value="experimental">Dark (Experimental)</option>
|
<option value="experimental">Dark (Experimental)</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -853,15 +853,15 @@
|
|||||||
<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" value="12" class="settings-slider" />
|
<input type="range" id="setting-font-size" min="10" max="18" step="1" class="settings-slider" x-model.number="fontSize" />
|
||||||
<span class="settings-value" id="setting-font-size-value">12px</span>
|
<span class="settings-value" id="setting-font-size-value" x-text="fontSize + 'px'"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<label class="settings-label" for="setting-editor-theme">Editor Theme</label>
|
<label class="settings-label" for="setting-editor-theme">Editor Theme</label>
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<select id="setting-editor-theme" class="settings-select">
|
<select id="setting-editor-theme" class="settings-select" x-model="editorTheme">
|
||||||
<option value="vs-light">Light</option>
|
<option value="vs-light">Light</option>
|
||||||
<option value="vs-dark">Dark</option>
|
<option value="vs-dark">Dark</option>
|
||||||
<option value="hc-black">High Contrast</option>
|
<option value="hc-black">High Contrast</option>
|
||||||
@@ -872,7 +872,7 @@
|
|||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<label class="settings-label" for="setting-tab-size">Tab Size</label>
|
<label class="settings-label" for="setting-tab-size">Tab Size</label>
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<select id="setting-tab-size" class="settings-select">
|
<select id="setting-tab-size" class="settings-select" x-model.number="tabSize">
|
||||||
<option value="2">2 spaces</option>
|
<option value="2">2 spaces</option>
|
||||||
<option value="4">4 spaces</option>
|
<option value="4">4 spaces</option>
|
||||||
<option value="8">8 spaces</option>
|
<option value="8">8 spaces</option>
|
||||||
@@ -882,21 +882,21 @@
|
|||||||
|
|
||||||
<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" />
|
<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" checked />
|
<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" checked />
|
<input type="checkbox" id="setting-line-numbers" class="settings-checkbox" x-model="lineNumbers" />
|
||||||
Show line numbers
|
Show line numbers
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -909,8 +909,8 @@
|
|||||||
<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" value="1500" class="settings-slider" />
|
<input type="range" id="setting-render-debounce" min="300" max="3000" step="100" class="settings-slider" x-model.number="renderDebounce" />
|
||||||
<span class="settings-value" id="setting-render-debounce-value">1500ms</span>
|
<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>
|
||||||
@@ -923,7 +923,7 @@
|
|||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<label class="settings-label" for="setting-date-format">Date Format</label>
|
<label class="settings-label" for="setting-date-format">Date Format</label>
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<select id="setting-date-format" class="settings-select">
|
<select id="setting-date-format" class="settings-select" x-model="dateFormat">
|
||||||
<option value="smart">Smart (relative times)</option>
|
<option value="smart">Smart (relative times)</option>
|
||||||
<option value="locale">Locale (browser default)</option>
|
<option value="locale">Locale (browser default)</option>
|
||||||
<option value="iso">ISO 8601</option>
|
<option value="iso">ISO 8601</option>
|
||||||
@@ -932,10 +932,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-item" id="custom-date-format-item" style="display: none;">
|
<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" value="yyyy-MM-dd HH:mm" placeholder="yyyy-MM-dd HH:mm" />
|
<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)
|
||||||
@@ -945,9 +945,19 @@
|
|||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="settings-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 primary"
|
||||||
<button class="btn btn-modal" id="settings-reset-btn" title="Reset to default settings">Reset to Defaults</button>
|
id="settings-apply-btn"
|
||||||
<button class="btn btn-modal" id="settings-cancel-btn" title="Cancel without saving">Cancel</button>
|
@click="apply()"
|
||||||
|
:disabled="!isDirty"
|
||||||
|
title="Apply and save settings">Apply</button>
|
||||||
|
<button class="btn btn-modal"
|
||||||
|
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>
|
||||||
|
|||||||
@@ -139,28 +139,50 @@ Incremental migration of Astrolabe from vanilla JavaScript to Alpine.js for reac
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 4: Settings Modal
|
## Phase 4: Settings Modal ✅ COMPLETE
|
||||||
|
|
||||||
**Status**: Planned
|
**Status**: Done
|
||||||
**Files**: `src/js/app.js`, `index.html`
|
**Files**: `src/js/user-settings.js`, `index.html`, `src/js/app.js`
|
||||||
|
|
||||||
### What to Convert
|
### What Was Converted
|
||||||
|
|
||||||
Settings form with multiple inputs and apply/reset functionality.
|
- Settings modal form with all input controls using `x-model`
|
||||||
|
- Apply/Reset/Cancel buttons with `@click` handlers
|
||||||
|
- Computed `isDirty` property to enable/disable Apply button
|
||||||
|
- Conditional custom date format field with `x-show`
|
||||||
|
- Slider value displays with `x-text`
|
||||||
|
- All form state management moved to Alpine component
|
||||||
|
|
||||||
### Implementation Approach
|
### Implementation Approach
|
||||||
|
|
||||||
1. Create `settingsPanel()` component with settings state tracking
|
1. Created `settingsPanel()` component in `user-settings.js` with form state tracking
|
||||||
2. Use `x-model` for all form inputs (theme, editor settings, date format)
|
2. Used `x-model` (with `.number` modifier where needed) for all form inputs:
|
||||||
3. Add computed `isDirty` property to enable/disable Apply button
|
- Theme selects
|
||||||
4. Use `x-show` for conditional fields (custom date format)
|
- Font size and render debounce sliders
|
||||||
5. Remove manual event listeners from app.js
|
- Tab size select
|
||||||
|
- Checkboxes (minimap, word wrap, line numbers)
|
||||||
|
- Date format and custom format input
|
||||||
|
3. Added computed `isDirty` getter to compare current vs. original state
|
||||||
|
4. Added computed `showCustomDateFormat` getter for conditional field visibility
|
||||||
|
5. Moved all apply/reset/cancel logic into Alpine component methods
|
||||||
|
6. Removed vanilla event listeners and old `loadSettingsIntoUI()` / `applySettings()` functions
|
||||||
|
|
||||||
### What Stays Vanilla
|
### What Stays Vanilla
|
||||||
|
|
||||||
- AppSettings storage layer
|
- Settings storage layer (getSettings, updateSettings, etc.)
|
||||||
- Editor option updates
|
- Settings validation logic (validateSetting)
|
||||||
- Theme application logic
|
- Editor option updates (applied from within Alpine component)
|
||||||
|
- Theme application logic (applied from within Alpine component)
|
||||||
|
- Modal open/close functions (simple ModalManager calls)
|
||||||
|
|
||||||
|
### Key Learnings
|
||||||
|
|
||||||
|
- Alpine component handles all form state reactivity
|
||||||
|
- `isDirty` computed property automatically enables/disables Apply button
|
||||||
|
- `x-model.number` modifier ensures numeric values for sliders and selects
|
||||||
|
- `x-show` provides clean conditional rendering for custom date format field
|
||||||
|
- All business logic (validation, saving, applying) stays in existing functions
|
||||||
|
- Net code reduction: ~150 lines of manual DOM manipulation removed
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -261,7 +283,7 @@ Toast notification system with auto-dismiss.
|
|||||||
1. ✅ **Phase 1: Snippet Panel** - DONE
|
1. ✅ **Phase 1: Snippet Panel** - DONE
|
||||||
2. ✅ **Phase 2: Dataset Manager** - DONE
|
2. ✅ **Phase 2: Dataset Manager** - DONE
|
||||||
3. ✅ **Phase 3: View Mode Toggle** - DONE
|
3. ✅ **Phase 3: View Mode Toggle** - DONE
|
||||||
4. **Phase 4: Settings Modal** - Another modal, builds confidence
|
4. ✅ **Phase 4: Settings Modal** - DONE
|
||||||
5. **Phase 6: Meta Fields** - Before Chart Builder (simpler)
|
5. **Phase 6: Meta Fields** - Before Chart Builder (simpler)
|
||||||
6. **Phase 7: Panel Toggles** - Quick win
|
6. **Phase 7: Panel Toggles** - Quick win
|
||||||
7. **Phase 5: Chart Builder** - More complex, save for when confident
|
7. **Phase 5: Chart Builder** - More complex, save for when confident
|
||||||
|
|||||||
203
src/js/app.js
203
src/js/app.js
@@ -184,61 +184,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// Settings Modal
|
// Settings Modal
|
||||||
const settingsLink = document.getElementById('settings-link');
|
const settingsLink = document.getElementById('settings-link');
|
||||||
const settingsApplyBtn = document.getElementById('settings-apply-btn');
|
|
||||||
const settingsResetBtn = document.getElementById('settings-reset-btn');
|
|
||||||
const settingsCancelBtn = document.getElementById('settings-cancel-btn');
|
|
||||||
|
|
||||||
if (settingsLink) {
|
if (settingsLink) {
|
||||||
settingsLink.addEventListener('click', openSettingsModal);
|
settingsLink.addEventListener('click', openSettingsModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsCancelBtn) {
|
// Settings buttons and UI interactions now handled by Alpine.js in settingsPanel() component
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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');
|
||||||
@@ -567,160 +518,12 @@ function registerMonacoKeyboardShortcuts() {
|
|||||||
KeyboardActions.publishDraft);
|
KeyboardActions.publishDraft);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings modal functions (special handling for loading settings into UI)
|
// Settings modal functions (simplified - most logic now in Alpine settingsPanel() component)
|
||||||
function openSettingsModal() {
|
function openSettingsModal() {
|
||||||
loadSettingsIntoUI();
|
|
||||||
ModalManager.open('settings-modal');
|
ModalManager.open('settings-modal');
|
||||||
|
// Settings will be loaded via Alpine's init() method
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSettingsModal() {
|
function closeSettingsModal() {
|
||||||
ModalManager.close('settings-modal');
|
ModalManager.close('settings-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSettingsIntoUI() {
|
|
||||||
const settings = getSettings();
|
|
||||||
|
|
||||||
// Appearance settings
|
|
||||||
const uiThemeSelect = document.getElementById('setting-ui-theme');
|
|
||||||
if (uiThemeSelect) {
|
|
||||||
uiThemeSelect.value = settings.ui.theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 editorThemeSelect = document.getElementById('setting-editor-theme');
|
|
||||||
if (editorThemeSelect) {
|
|
||||||
editorThemeSelect.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 = {
|
|
||||||
'ui.theme': document.getElementById('setting-ui-theme').value,
|
|
||||||
'editor.fontSize': parseInt(document.getElementById('setting-font-size').value),
|
|
||||||
'editor.theme': document.getElementById('setting-editor-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 theme to document
|
|
||||||
const uiTheme = newSettings['ui.theme'];
|
|
||||||
document.documentElement.setAttribute('data-theme', uiTheme);
|
|
||||||
|
|
||||||
// Sync editor theme with UI theme
|
|
||||||
const editorTheme = uiTheme === 'experimental' ? 'vs-dark' : 'vs-light';
|
|
||||||
newSettings['editor.theme'] = editorTheme;
|
|
||||||
|
|
||||||
// Apply editor settings immediately
|
|
||||||
if (editor) {
|
|
||||||
editor.updateOptions({
|
|
||||||
fontSize: newSettings['editor.fontSize'],
|
|
||||||
theme: editorTheme,
|
|
||||||
tabSize: newSettings['editor.tabSize'],
|
|
||||||
minimap: { enabled: newSettings['editor.minimap'] },
|
|
||||||
wordWrap: newSettings['editor.wordWrap'],
|
|
||||||
lineNumbers: newSettings['editor.lineNumbers']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the editor theme in settings
|
|
||||||
updateSetting('editor.theme', editorTheme);
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -222,3 +222,166 @@ function validateSetting(path, value) {
|
|||||||
|
|
||||||
return rules[path] ? rules[path]() : [];
|
return rules[path] ? rules[path]() : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alpine.js Component for settings panel
|
||||||
|
// Thin wrapper - Alpine handles form state and reactivity, user-settings.js handles storage
|
||||||
|
function settingsPanel() {
|
||||||
|
return {
|
||||||
|
// Form state (loaded from settings on open)
|
||||||
|
uiTheme: 'light',
|
||||||
|
fontSize: 12,
|
||||||
|
editorTheme: 'vs-light',
|
||||||
|
tabSize: 2,
|
||||||
|
minimap: false,
|
||||||
|
wordWrap: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
renderDebounce: 1500,
|
||||||
|
dateFormat: 'smart',
|
||||||
|
customDateFormat: 'yyyy-MM-dd HH:mm',
|
||||||
|
|
||||||
|
// Original values for dirty checking
|
||||||
|
originalSettings: null,
|
||||||
|
|
||||||
|
// Initialize component with current settings
|
||||||
|
init() {
|
||||||
|
this.loadSettings();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load settings from storage into form
|
||||||
|
loadSettings() {
|
||||||
|
const settings = getSettings();
|
||||||
|
this.uiTheme = settings.ui.theme;
|
||||||
|
this.fontSize = settings.editor.fontSize;
|
||||||
|
this.editorTheme = settings.editor.theme;
|
||||||
|
this.tabSize = settings.editor.tabSize;
|
||||||
|
this.minimap = settings.editor.minimap;
|
||||||
|
this.wordWrap = settings.editor.wordWrap === 'on';
|
||||||
|
this.lineNumbers = settings.editor.lineNumbers === 'on';
|
||||||
|
this.renderDebounce = settings.performance.renderDebounce;
|
||||||
|
this.dateFormat = settings.formatting.dateFormat;
|
||||||
|
this.customDateFormat = settings.formatting.customDateFormat;
|
||||||
|
|
||||||
|
// Store original values for dirty checking
|
||||||
|
this.originalSettings = JSON.stringify(this.getCurrentFormState());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get current form state as object
|
||||||
|
getCurrentFormState() {
|
||||||
|
return {
|
||||||
|
uiTheme: this.uiTheme,
|
||||||
|
fontSize: this.fontSize,
|
||||||
|
editorTheme: this.editorTheme,
|
||||||
|
tabSize: this.tabSize,
|
||||||
|
minimap: this.minimap,
|
||||||
|
wordWrap: this.wordWrap,
|
||||||
|
lineNumbers: this.lineNumbers,
|
||||||
|
renderDebounce: this.renderDebounce,
|
||||||
|
dateFormat: this.dateFormat,
|
||||||
|
customDateFormat: this.customDateFormat
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if settings have been modified
|
||||||
|
get isDirty() {
|
||||||
|
return this.originalSettings !== JSON.stringify(this.getCurrentFormState());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show custom date format field when 'custom' is selected
|
||||||
|
get showCustomDateFormat() {
|
||||||
|
return this.dateFormat === 'custom';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Apply settings and save
|
||||||
|
apply() {
|
||||||
|
const newSettings = {
|
||||||
|
'ui.theme': this.uiTheme,
|
||||||
|
'editor.fontSize': parseInt(this.fontSize),
|
||||||
|
'editor.theme': this.editorTheme,
|
||||||
|
'editor.tabSize': parseInt(this.tabSize),
|
||||||
|
'editor.minimap': this.minimap,
|
||||||
|
'editor.wordWrap': this.wordWrap ? 'on' : 'off',
|
||||||
|
'editor.lineNumbers': this.lineNumbers ? 'on' : 'off',
|
||||||
|
'performance.renderDebounce': parseInt(this.renderDebounce),
|
||||||
|
'formatting.dateFormat': this.dateFormat,
|
||||||
|
'formatting.customDateFormat': this.customDateFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 theme to document
|
||||||
|
document.documentElement.setAttribute('data-theme', this.uiTheme);
|
||||||
|
|
||||||
|
// Sync editor theme with UI theme
|
||||||
|
const editorTheme = this.uiTheme === 'experimental' ? 'vs-dark' : 'vs-light';
|
||||||
|
newSettings['editor.theme'] = editorTheme;
|
||||||
|
|
||||||
|
// Apply editor settings immediately
|
||||||
|
if (editor) {
|
||||||
|
editor.updateOptions({
|
||||||
|
fontSize: newSettings['editor.fontSize'],
|
||||||
|
theme: editorTheme,
|
||||||
|
tabSize: newSettings['editor.tabSize'],
|
||||||
|
minimap: { enabled: newSettings['editor.minimap'] },
|
||||||
|
wordWrap: newSettings['editor.wordWrap'],
|
||||||
|
lineNumbers: newSettings['editor.lineNumbers']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the editor theme in settings
|
||||||
|
updateSetting('editor.theme', editorTheme);
|
||||||
|
|
||||||
|
// 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.success('Settings applied successfully');
|
||||||
|
closeSettingsModal();
|
||||||
|
|
||||||
|
// Track event
|
||||||
|
Analytics.track('settings-apply', 'Applied settings');
|
||||||
|
} else {
|
||||||
|
Toast.error('Failed to save settings');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reset to defaults
|
||||||
|
reset() {
|
||||||
|
if (confirm('Reset all settings to defaults? This cannot be undone.')) {
|
||||||
|
resetSettings();
|
||||||
|
this.loadSettings();
|
||||||
|
Toast.success('Settings reset to defaults');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cancel changes and close modal
|
||||||
|
cancel() {
|
||||||
|
closeSettingsModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user