diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index 5b57c093dd..856250e44c 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -114,13 +114,34 @@ let settingsRef: RemovableRef | null = null */ export function useSettings() { if (!settingsRef) { - settingsRef = useLocalStorage(STORAGE_KEY, DEFAULT_SETTINGS, { + // Clone so that changing settings never mutates DEFAULT_SETTINGS + settingsRef = useLocalStorage(STORAGE_KEY, structuredClone(DEFAULT_SETTINGS), { mergeDefaults: true, }) } return { settings: settingsRef, + resetSettings, + } +} + +/** + * Restore all settings to their defaults, keeping the selected language. + */ +function resetSettings() { + if (!settingsRef) return + + settingsRef.value = { + ...structuredClone(DEFAULT_SETTINGS), + selectedLocale: settingsRef.value.selectedLocale, + } + + if (import.meta.client) { + document.documentElement.style.removeProperty('--accent-color') + document.documentElement.removeAttribute('data-bg-theme') + document.documentElement.removeAttribute('data-fg-theme') + document.documentElement.removeAttribute('data-collapsed') } } diff --git a/app/pages/settings.vue b/app/pages/settings.vue index bd05b2c393..ff430d329d 100644 --- a/app/pages/settings.vue +++ b/app/pages/settings.vue @@ -2,7 +2,7 @@ import type { LocaleObject } from '@nuxtjs/i18n' const router = useRouter() -const { settings } = useSettings() +const { settings, resetSettings } = useSettings() const { locale, locales, setLocale: setNuxti18nLocale } = useI18n() const colorMode = useColorMode() const { currentLocaleStatus, isSourceLocale } = useI18nStatus() @@ -28,6 +28,14 @@ const currentLocale = computed({ }, }) +const isConfirmingReset = shallowRef(false) + +function onResetSettings() { + resetSettings() + colorMode.preference = 'system' + isConfirmingReset.value = false +} + // Escape to go back (but not when focused on form elements or modal is open) onKeyStroke( e => @@ -333,6 +341,32 @@ useSeoMeta({ /> + + +
+

+ {{ $t('settings.sections.reset') }} +

+
+

+ {{ $t('settings.reset_settings_description') }} +

+
+ + {{ $t('settings.reset_settings_confirm') }} + + + {{ $t('settings.sections.reset') }} + + + {{ $t('common.cancel') }} + +
+ + {{ $t('settings.reset_settings') }} + +
+
diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 5c3e2a9341..e56d312821 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -315,7 +315,8 @@ "display": "Display", "search": "Search features", "language": "Language", - "keyboard_shortcuts": "Keyboard shortcuts" + "keyboard_shortcuts": "Keyboard shortcuts", + "reset": "Reset" }, "data_source": { "label": "Data source", @@ -371,7 +372,10 @@ "keyboard_shortcuts_enabled_description": "Keyboard shortcuts can be disabled if they conflict with other browser or system shortcuts", "enable_code_ligatures": "Enable ligatures in code", "enable_changelog_autoscroll": "Auto scroll to requested version", - "enable_changelog_autoscroll_description": "Auto scroll to or close to the requested version at package changelog" + "enable_changelog_autoscroll_description": "Auto scroll to or close to the requested version at package changelog", + "reset_settings": "Reset all settings", + "reset_settings_description": "Restore every setting on this page to its default value. Your language is kept.", + "reset_settings_confirm": "Reset all settings to their defaults?" }, "i18n": { "missing_keys": "{count} missing translation | {count} missing translations", diff --git a/i18n/schema.json b/i18n/schema.json index 1619369786..55143e4e0e 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -951,6 +951,9 @@ }, "keyboard_shortcuts": { "type": "string" + }, + "reset": { + "type": "string" } }, "additionalProperties": false @@ -1119,6 +1122,15 @@ }, "enable_changelog_autoscroll_description": { "type": "string" + }, + "reset_settings": { + "type": "string" + }, + "reset_settings_description": { + "type": "string" + }, + "reset_settings_confirm": { + "type": "string" } }, "additionalProperties": false diff --git a/test/nuxt/composables/use-settings.spec.ts b/test/nuxt/composables/use-settings.spec.ts index 74eb043450..93f5c3a33a 100644 --- a/test/nuxt/composables/use-settings.spec.ts +++ b/test/nuxt/composables/use-settings.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' describe('useSettings - keyboardShortcuts', () => { beforeEach(() => { @@ -74,3 +74,50 @@ describe('useSettings - codeLigatures', () => { }) }) }) + +describe('useSettings - resetSettings', () => { + beforeEach(() => { + vi.resetModules() + }) + + // Persisted settings (e.g. selectedLocale) would leak into other test files + afterEach(() => { + localStorage.clear() + }) + + it('restores defaults and keeps the selected locale', async () => { + const { useSettings } = await import('~/composables/useSettings') + const { settings, resetSettings } = useSettings() + + settings.value.relativeDates = true + settings.value.keyboardShortcuts = false + settings.value.accentColorId = 'coral' + settings.value.sidebar.collapsed = ['readme'] + settings.value.selectedLocale = 'fr-FR' + + resetSettings() + + expect(settings.value.relativeDates).toBe(false) + expect(settings.value.keyboardShortcuts).toBe(true) + expect(settings.value.accentColorId).toBeNull() + expect(settings.value.sidebar.collapsed).toEqual([]) + expect(settings.value.selectedLocale).toBe('fr-FR') + }) + + it('clears theme attributes from the document', async () => { + const { useSettings } = await import('~/composables/useSettings') + const { resetSettings } = useSettings() + + document.documentElement.style.setProperty('--accent-color', 'var(--swatch-coral)') + document.documentElement.dataset.bgTheme = 'slate' + document.documentElement.dataset.fgTheme = 'contrast' + document.documentElement.dataset.collapsed = 'readme' + + resetSettings() + + expect(document.documentElement.style.getPropertyValue('--accent-color')).toBe('') + expect(document.documentElement.dataset.bgTheme).toBeUndefined() + expect(document.documentElement.dataset.fgTheme).toBeUndefined() + expect(document.documentElement.dataset.collapsed).toBeUndefined() + }) +})