+
diff --git a/settings.js b/settings.js
index fada367..7ba486b 100644
--- a/settings.js
+++ b/settings.js
@@ -316,16 +316,15 @@ document.addEventListener('DOMContentLoaded', () => {
// ----------------------------------------
// FOCUS MODE BINDINGS
// ----------------------------------------
- const focusModeEnabledCheckbox = document.getElementById('focus-mode-enabled-checkbox');
- const focusModeControls = document.getElementById('focusModeControls');
- const focusModeDimOpacitySlider = document.getElementById('focus-mode-dim-opacity-slider');
- const focusModeTimeoutSlider = document.getElementById('focus-mode-timeout-slider');
- const focusModeDimOpacityLabel = document.getElementById('val-focus-mode-dim-opacity');
- const focusModeTimeoutLabel = document.getElementById('val-focus-mode-timeout');
+ const focusModeCheckbox = document.getElementById('focus-mode-checkbox');
+ const focusModeSettingsContainer = document.getElementById('focus-mode-settings-container');
+ const focusModeDimOpacity = document.getElementById('focus-mode-dim-opacity');
+ const focusModeIdleTimeout = document.getElementById('focus-mode-idle-timeout');
+ const focusModeTransitionDuration = document.getElementById('focus-mode-transition-duration');
function toggleFocusModeControls(isEnabled) {
- if (focusModeControls) {
- focusModeControls.style.display = isEnabled ? 'block' : 'none';
+ if (focusModeSettingsContainer) {
+ focusModeSettingsContainer.style.display = isEnabled ? 'block' : 'none';
}
}
@@ -333,41 +332,48 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.visualizerSettings) {
const currentFocusMode = cachedSettings.focusMode || {};
const nextFocusMode = { ...currentFocusMode, ...patch };
- cachedSettings.focusMode = nextFocusMode; // Optimistic local cache update!
+ cachedSettings.focusMode = nextFocusMode; // Optimistic local cache update
window.visualizerSettings.update({
focusMode: nextFocusMode
});
}
}
- if (focusModeEnabledCheckbox) {
- focusModeEnabledCheckbox.addEventListener('change', (e) => {
+ if (focusModeCheckbox) {
+ focusModeCheckbox.addEventListener('change', (e) => {
const isChecked = e.target.checked;
toggleFocusModeControls(isChecked);
updateFocusModeSetting({ enabled: isChecked });
});
}
- if (focusModeDimOpacitySlider) {
- focusModeDimOpacitySlider.addEventListener('input', (e) => {
- const val = parseFloat(e.target.value);
- if (focusModeDimOpacityLabel) {
- focusModeDimOpacityLabel.textContent = val.toFixed(2);
- }
+ if (focusModeDimOpacity) {
+ focusModeDimOpacity.addEventListener('input', (e) => {
+ const val = parseFloat(e.target.value) / 100;
+ const valEl = document.getElementById('val-focus-mode-dim-opacity');
+ if (valEl) valEl.textContent = `${e.target.value}%`;
updateFocusModeSetting({ dimOpacity: val });
});
}
- if (focusModeTimeoutSlider) {
- focusModeTimeoutSlider.addEventListener('input', (e) => {
- const val = parseInt(e.target.value, 10);
- if (focusModeTimeoutLabel) {
- focusModeTimeoutLabel.textContent = val;
- }
+ if (focusModeIdleTimeout) {
+ focusModeIdleTimeout.addEventListener('input', (e) => {
+ const val = parseInt(e.target.value, 10) || 5;
+ const valEl = document.getElementById('val-focus-mode-idle-timeout');
+ if (valEl) valEl.textContent = `${val}s`;
updateFocusModeSetting({ idleTimeout: val });
});
}
+ if (focusModeTransitionDuration) {
+ focusModeTransitionDuration.addEventListener('input', (e) => {
+ const val = parseFloat(e.target.value) || 1.5;
+ const valEl = document.getElementById('val-focus-mode-transition-duration');
+ if (valEl) valEl.textContent = `${val.toFixed(1)}s`;
+ updateFocusModeSetting({ transitionDuration: val });
+ });
+ }
+
const themeSelector = document.getElementById('theme-selector');
if (themeSelector) {
themeSelector.addEventListener('change', (e) => {
@@ -383,6 +389,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
+
const performanceModeSelector = document.getElementById('performance-mode-selector');
if (performanceModeSelector) {
performanceModeSelector.addEventListener('change', (e) => {
@@ -1356,21 +1363,27 @@ refreshThemeProfiles();
// Load focus mode settings
if (settings.focusMode) {
const fm = settings.focusMode;
- if (focusModeEnabledCheckbox) {
- focusModeEnabledCheckbox.checked = !!fm.enabled;
+ if (focusModeCheckbox) {
+ focusModeCheckbox.checked = !!fm.enabled;
toggleFocusModeControls(fm.enabled);
}
- if (focusModeDimOpacitySlider) {
- focusModeDimOpacitySlider.value = fm.dimOpacity !== undefined ? fm.dimOpacity : 0.1;
- if (focusModeDimOpacityLabel) {
- focusModeDimOpacityLabel.textContent = parseFloat(focusModeDimOpacitySlider.value).toFixed(2);
- }
+ if (focusModeDimOpacity) {
+ const pct = Math.round((fm.dimOpacity !== undefined ? fm.dimOpacity : 0.1) * 100);
+ focusModeDimOpacity.value = pct;
+ const valEl = document.getElementById('val-focus-mode-dim-opacity');
+ if (valEl) valEl.textContent = `${pct}%`;
}
- if (focusModeTimeoutSlider) {
- focusModeTimeoutSlider.value = fm.idleTimeout !== undefined ? fm.idleTimeout : 5;
- if (focusModeTimeoutLabel) {
- focusModeTimeoutLabel.textContent = focusModeTimeoutSlider.value;
- }
+ if (focusModeIdleTimeout) {
+ const secs = fm.idleTimeout !== undefined ? fm.idleTimeout : 5;
+ focusModeIdleTimeout.value = secs;
+ const valEl = document.getElementById('val-focus-mode-idle-timeout');
+ if (valEl) valEl.textContent = `${secs}s`;
+ }
+ if (focusModeTransitionDuration) {
+ const duration = fm.transitionDuration !== undefined ? fm.transitionDuration : 1.5;
+ focusModeTransitionDuration.value = duration;
+ const valEl = document.getElementById('val-focus-mode-transition-duration');
+ if (valEl) valEl.textContent = `${duration.toFixed(1)}s`;
}
}
@@ -1464,24 +1477,28 @@ refreshThemeProfiles();
updateThemeLabels(dayStart, nightStart);
}
- // Sync Focus Mode settings
+ // Sync focus mode properties if updated from outside
if (nextSettings.focusMode) {
const fm = nextSettings.focusMode;
- if (focusModeEnabledCheckbox && fm.enabled !== undefined) {
- focusModeEnabledCheckbox.checked = !!fm.enabled;
+ if (focusModeCheckbox && fm.enabled !== undefined) {
+ focusModeCheckbox.checked = !!fm.enabled;
toggleFocusModeControls(fm.enabled);
}
- if (focusModeDimOpacitySlider && fm.dimOpacity !== undefined) {
- focusModeDimOpacitySlider.value = fm.dimOpacity;
- if (focusModeDimOpacityLabel) {
- focusModeDimOpacityLabel.textContent = parseFloat(fm.dimOpacity).toFixed(2);
- }
+ if (focusModeDimOpacity && fm.dimOpacity !== undefined) {
+ const pct = Math.round(fm.dimOpacity * 100);
+ focusModeDimOpacity.value = pct;
+ const valEl = document.getElementById('val-focus-mode-dim-opacity');
+ if (valEl) valEl.textContent = `${pct}%`;
}
- if (focusModeTimeoutSlider && fm.idleTimeout !== undefined) {
- focusModeTimeoutSlider.value = fm.idleTimeout;
- if (focusModeTimeoutLabel) {
- focusModeTimeoutLabel.textContent = fm.idleTimeout;
- }
+ if (focusModeIdleTimeout && fm.idleTimeout !== undefined) {
+ focusModeIdleTimeout.value = fm.idleTimeout;
+ const valEl = document.getElementById('val-focus-mode-idle-timeout');
+ if (valEl) valEl.textContent = `${fm.idleTimeout}s`;
+ }
+ if (focusModeTransitionDuration && fm.transitionDuration !== undefined) {
+ focusModeTransitionDuration.value = fm.transitionDuration;
+ const valEl = document.getElementById('val-focus-mode-transition-duration');
+ if (valEl) valEl.textContent = `${fm.transitionDuration.toFixed(1)}s`;
}
}
diff --git a/settingsStore.js b/settingsStore.js
index 4426231..a4a9df5 100644
--- a/settingsStore.js
+++ b/settingsStore.js
@@ -115,7 +115,8 @@ const DEFAULT_SETTINGS = Object.freeze({
focusMode: Object.freeze({
enabled: false,
dimOpacity: 0.1,
- idleTimeout: 5
+ idleTimeout: 5,
+ transitionDuration: 1.5
}),
auroraDrift: Object.freeze({
// Standard settings
@@ -572,13 +573,16 @@ function migrateLegacySettings(input = {}) {
function sanitizeFocusMode(input = {}) {
const enabled = typeof input.enabled === "boolean" ? input.enabled : DEFAULT_SETTINGS.focusMode.enabled;
- const dimOpacity = typeof input.dimOpacity === "number" && input.dimOpacity >= 0 && input.dimOpacity <= 1
- ? input.dimOpacity
+ const dimOpacity = typeof input.dimOpacity === "number" && Number.isFinite(input.dimOpacity)
+ ? Math.max(0, Math.min(1, input.dimOpacity))
: DEFAULT_SETTINGS.focusMode.dimOpacity;
- const idleTimeout = typeof input.idleTimeout === "number" && input.idleTimeout >= 1 && input.idleTimeout <= 60
- ? input.idleTimeout
+ const idleTimeout = typeof input.idleTimeout === "number" && Number.isFinite(input.idleTimeout)
+ ? Math.max(1, Math.min(60, input.idleTimeout))
: DEFAULT_SETTINGS.focusMode.idleTimeout;
- return { enabled, dimOpacity, idleTimeout };
+ const transitionDuration = typeof input.transitionDuration === "number" && Number.isFinite(input.transitionDuration)
+ ? Math.max(0.1, Math.min(10, input.transitionDuration))
+ : DEFAULT_SETTINGS.focusMode.transitionDuration;
+ return { enabled, dimOpacity, idleTimeout, transitionDuration };
}
function sanitizeShortcuts(input) {
diff --git a/test/settingsStore.test.js b/test/settingsStore.test.js
index 9991bdf..40b380b 100644
--- a/test/settingsStore.test.js
+++ b/test/settingsStore.test.js
@@ -119,3 +119,36 @@ test("settingsStore - Legacy Theme conversion (sensitivity mapping)", () => {
assert.strictEqual(sanitized.selectedTheme, "reactiveBorder");
assert.strictEqual(sanitized.reactiveBorder.intensity, "high");
});
+
+test("settingsStore - Focus Mode sanitization and clamping", () => {
+ const input = {
+ selectedTheme: "ambientWave",
+ focusMode: {
+ enabled: true,
+ dimOpacity: 1.5, // should clamp to 1
+ idleTimeout: 75, // should clamp to 60
+ transitionDuration: 15 // should clamp to 10
+ }
+ };
+ const sanitized = sanitizeSettings(input);
+ assert.strictEqual(sanitized.focusMode.enabled, true);
+ assert.strictEqual(sanitized.focusMode.dimOpacity, 1);
+ assert.strictEqual(sanitized.focusMode.idleTimeout, 60);
+ assert.strictEqual(sanitized.focusMode.transitionDuration, 10);
+
+ const inputMin = {
+ selectedTheme: "ambientWave",
+ focusMode: {
+ enabled: false,
+ dimOpacity: -0.5, // should clamp to 0
+ idleTimeout: 0, // should clamp to 1
+ transitionDuration: 0.05 // should clamp to 0.1
+ }
+ };
+ const sanitizedMin = sanitizeSettings(inputMin);
+ assert.strictEqual(sanitizedMin.focusMode.enabled, false);
+ assert.strictEqual(sanitizedMin.focusMode.dimOpacity, 0);
+ assert.strictEqual(sanitizedMin.focusMode.idleTimeout, 1);
+ assert.strictEqual(sanitizedMin.focusMode.transitionDuration, 0.1);
+});
+