Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion app/composables/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,34 @@ let settingsRef: RemovableRef<AppSettings> | null = null
*/
export function useSettings() {
if (!settingsRef) {
settingsRef = useLocalStorage<AppSettings>(STORAGE_KEY, DEFAULT_SETTINGS, {
// Clone so that changing settings never mutates DEFAULT_SETTINGS
settingsRef = useLocalStorage<AppSettings>(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')
}
}

Expand Down
36 changes: 35 additions & 1 deletion app/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -28,6 +28,14 @@ const currentLocale = computed<string>({
},
})

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 =>
Expand Down Expand Up @@ -333,6 +341,32 @@ useSeoMeta({
/>
</div>
</section>

<!-- RESET Section -->
<section>
<h2 class="text-xs text-fg-muted uppercase tracking-wider mb-4">
{{ $t('settings.sections.reset') }}
</h2>
<div class="bg-bg-subtle border border-border rounded-lg p-4 sm:p-6 space-y-3">
<p class="text-xs text-fg-muted">
{{ $t('settings.reset_settings_description') }}
</p>
<div v-if="isConfirmingReset" class="flex flex-wrap items-center gap-2">
<span class="text-sm text-fg" role="status">
{{ $t('settings.reset_settings_confirm') }}
</span>
<ButtonBase variant="primary" @click="onResetSettings">
{{ $t('settings.sections.reset') }}
</ButtonBase>
<ButtonBase @click="isConfirmingReset = false">
{{ $t('common.cancel') }}
</ButtonBase>
</div>
<ButtonBase v-else classicon="i-lucide:rotate-ccw" @click="isConfirmingReset = true">
{{ $t('settings.reset_settings') }}
</ButtonBase>
</div>
</section>
</div>
</article>
</main>
Expand Down
8 changes: 6 additions & 2 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,9 @@
},
"keyboard_shortcuts": {
"type": "string"
},
"reset": {
"type": "string"
}
},
"additionalProperties": false
Expand Down Expand Up @@ -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
Expand Down
49 changes: 48 additions & 1 deletion test/nuxt/composables/use-settings.spec.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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()
})
})
Loading