From f485a2d476b21c198462ee961923ad1b3f72e1ec Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 6 Aug 2026 15:24:20 +0200 Subject: [PATCH 1/6] feat(color-scheme): add color scheme settings Added an appearance settings section containing a color scheme setting and translations for that. --- src/features/settings/settings.json | 31 +++++++++++++++++++++++++++-- src/locales/de.json | 15 ++++++++++++++ src/locales/en.json | 15 ++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/features/settings/settings.json b/src/features/settings/settings.json index 1e36b85..76a4f69 100644 --- a/src/features/settings/settings.json +++ b/src/features/settings/settings.json @@ -1,3 +1,30 @@ { - "contents": [] -} \ No newline at end of file + "contents": [ + { + "name": "appearance", + "i18n": "settings.settings.appearance.title", + "content": [ + { + "name": "colorScheme", + "type": "selection", + "i18n": "settings.settings.appearance.contents.colorScheme.title", + "default": "auto", + "options": [ + { + "name": "auto", + "i18n": "settings.settings.appearance.contents.colorScheme.options.auto" + }, + { + "name": "light", + "i18n": "settings.settings.appearance.contents.colorScheme.options.light" + }, + { + "name": "dark", + "i18n": "settings.settings.appearance.contents.colorScheme.options.dark" + } + ] + } + ] + } + ] +} diff --git a/src/locales/de.json b/src/locales/de.json index 1b82c31..93f7593 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -60,6 +60,21 @@ "option": { "ariaLabel": "Wählt die Option {option} aus" } + }, + "settings": { + "appearance": { + "title": "Erscheinungsbild", + "contents": { + "colorScheme": { + "title": "Farbschema", + "options": { + "auto": "An Systemfarbschema anpassen", + "light": "Hell", + "dark": "Dunkel" + } + } + } + } } } } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index c47a8c1..4c4c8a1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -60,6 +60,21 @@ "option": { "ariaLabel": "Selects option {option}" } + }, + "settings": { + "appearance": { + "title": "Appearance", + "contents": { + "colorScheme": { + "title": "Color Scheme", + "options": { + "auto": "Adapt to system color scheme", + "light": "Light", + "dark": "Dark" + } + } + } + } } } } \ No newline at end of file -- 2.39.5 From f9a53a8911db931ab6b2f0c29c599eb68b37316b Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 6 Aug 2026 19:38:52 +0200 Subject: [PATCH 2/6] feat(settings)!: make settings module import static Migrated the settings to a static settings configuration import. --- .../composables/__tests__/useSettings.test.js | 49 +- .../settings/composables/useSettings.js | 4 +- .../settings/composables/useSettingsConfig.js | 48 -- .../stores/settingsConfigCacheStore.js | 31 - .../utils/__tests__/settingsValidator.test.js | 581 +++++++++--------- src/features/settings/utils/settingsParser.js | 41 -- .../settings/utils/settingsValidator.js | 21 +- src/features/settings/views/SettingsView.vue | 49 +- .../views/__tests__/SettingsView.test.js | 44 +- src/main.js | 5 + 10 files changed, 357 insertions(+), 516 deletions(-) delete mode 100644 src/features/settings/composables/useSettingsConfig.js delete mode 100644 src/features/settings/stores/settingsConfigCacheStore.js delete mode 100644 src/features/settings/utils/settingsParser.js diff --git a/src/features/settings/composables/__tests__/useSettings.test.js b/src/features/settings/composables/__tests__/useSettings.test.js index dc62d69..ddcdba8 100644 --- a/src/features/settings/composables/__tests__/useSettings.test.js +++ b/src/features/settings/composables/__tests__/useSettings.test.js @@ -14,16 +14,39 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useSettingsConfigCacheStore } from '../../stores/settingsConfigCacheStore'; import { useSettingsStore } from '../../stores/settingsStore'; import { useSettings } from '../useSettings'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -const settingDefaultValue = 42; +const settingDefaultValue = vi.hoisted(() => 42); const settingPath = ['test', 'number']; +vi.mock('../../settings.json', () => ({ + default: { + contents: [ + { + name: 'test', + i18n: '', + content: [ + { + type: 'number', + i18n: '', + name: 'number', + default: settingDefaultValue + }, + { + type: 'bool', + i18n: '', + name: 'bool' + } + ] + } + ] + } +})); + describe('useSettings', () => { describe('getSetting', () => { test('returns stored value', () => { @@ -69,26 +92,4 @@ beforeEach(() => { stubActions: false }) ); - const settingsConfigCache = useSettingsConfigCacheStore(); - settingsConfigCache.set({ - contents: [ - { - name: 'test', - i18n: '', - content: [ - { - type: 'number', - i18n: '', - name: 'number', - default: settingDefaultValue - }, - { - type: 'bool', - i18n: '', - name: 'bool' - } - ] - } - ] - }); }); diff --git a/src/features/settings/composables/useSettings.js b/src/features/settings/composables/useSettings.js index 4ad9818..e87e839 100644 --- a/src/features/settings/composables/useSettings.js +++ b/src/features/settings/composables/useSettings.js @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useSettingsConfigCacheStore } from '../stores/settingsConfigCacheStore'; +import settings from '../settings.json'; import { useSettingsStore } from '../stores/settingsStore'; import { getSettingRecursively } from '../utils/getSetting'; -import { loadSettingsConfig } from '../utils/settingsParser'; /** * Provides access to the stored settings. @@ -31,7 +30,6 @@ export const useSettings = function useSettings () { const getSetting = function getSetting (key) { const settingsStore = useSettingsStore(); - const settings = useSettingsConfigCacheStore().get(); return ( settingsStore.get(key) ?? getSettingRecursively( diff --git a/src/features/settings/composables/useSettingsConfig.js b/src/features/settings/composables/useSettingsConfig.js deleted file mode 100644 index 06d4bd8..0000000 --- a/src/features/settings/composables/useSettingsConfig.js +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2026 Seekra - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { ref, readonly } from 'vue'; -import { loadSettingsConfig } from '../utils/settingsParser.js'; - -const config = ref(null); -const error = ref(null); -const loading = ref(false); - -/** - * Provides reactive access to the parsed settings configuration. - * The config is loaded once and shared across all consumers. - */ -export function useSettingsConfig() { - async function load() { - loading.value = true; - error.value = null; - try { - config.value = await loadSettingsConfig(); - } catch (e) { - error.value = e.message; - config.value = null; - } finally { - loading.value = false; - } - } - - return { - config: readonly(config), - error: readonly(error), - loading: readonly(loading), - load, - }; -} \ No newline at end of file diff --git a/src/features/settings/stores/settingsConfigCacheStore.js b/src/features/settings/stores/settingsConfigCacheStore.js deleted file mode 100644 index 9ed923f..0000000 --- a/src/features/settings/stores/settingsConfigCacheStore.js +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2026 Seekra - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { defineStore } from 'pinia'; - -export const useSettingsConfigCacheStore = defineStore('settingsConfigCache', () => { - let settingsConfig = undefined; - - const set = function set (value) { - settingsConfig = value; - }; - - const get = function get () { - return settingsConfig; - }; - - return { set, get }; -}); diff --git a/src/features/settings/utils/__tests__/settingsValidator.test.js b/src/features/settings/utils/__tests__/settingsValidator.test.js index c0b0e8b..d6ea39c 100644 --- a/src/features/settings/utils/__tests__/settingsValidator.test.js +++ b/src/features/settings/utils/__tests__/settingsValidator.test.js @@ -20,18 +20,10 @@ import { validateSettingsConfig, validateEntry, validateSelectionOptions, assert describe('settingsValidator', () => { describe('validateSettingsConfig', () => { test.for([ - { raw: false, expected: false }, - { raw: true, expected: false }, - { raw: 0, expected: false }, - { raw: 42, expected: false }, - { raw: '', expected: false }, - { raw: ' ', expected: false }, - { raw: 'a', expected: false }, - { raw: {}, expected: false }, - { raw: { + { settings: { contents: [] - }, expected: true }, - { raw: { + } }, + { settings: { contents: [ { name: 'general', @@ -45,8 +37,8 @@ describe('settingsValidator', () => { ] } ] - }, expected: true }, - { raw: { + } }, + { settings: { contents: [ { name: 'general', @@ -71,8 +63,8 @@ describe('settingsValidator', () => { ] }, ] - }, expected: true }, - { raw: { + } }, + { settings: { contents: [ { name: 'general', @@ -128,12 +120,12 @@ describe('settingsValidator', () => { default: 'str' } ] - }, + } ] - }, + } ] - }, expected: true }, - { raw: { + } }, + { settings: { contents: [ { name: 'general', @@ -190,12 +182,283 @@ describe('settingsValidator', () => { default: 'str' } ] - }, + } + ] + } + ] + } } + ])('does not throw an error and returns input', ({ settings }) => { + validateSettingsConfig(settings); + }); + + test.for([ + { settings: false }, + { settings: true }, + { settings: 0 }, + { settings: 42 }, + { settings: '' }, + { settings: ' ' }, + { settings: 'a' }, + { settings: {} }, + { settings: { + content: [ + { + type: 'sectio', + name: 'general', + i18n: 'settings.settings.general', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + i18n: 'settings.settings.general.enableFeature42' + } ] }, + { + type: 'section', + name: 'copyOfGeneral', + i18n: 'settings.settings.copyOfGeneral', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + default: false, + i18n: 'settings.settings.general.enableFeature42' + } + ] + }, + { + type: 'section', + name: 'aSection', + i18n: 'settings.settings.aSection', + content: [ + { + type: 'bool', + name: 'Enable feature 43', + i18n: 'settings.settings.aSection.enableFeature43' + }, + { + type: 'selection', + name: 'language', + i18n: 'settings.settings.aSection.language.label', + default: 'en', + allowMultiple: 'false', + options: [ + { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, + { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, + ] + }, + { + type: 'section', + name: 'section2', + i18n: 'settings.settings.aSection.section2.label', + content: [ + { + type: 'string', + name: 'string', + i18n: 'settings.settings.aSection.sections.string', + default: 'str' + } + ] + } + ] + } ] - }, expected: true }, - { raw: { + } }, + { settings: { + content: [ + { + type: 'sectio', + name: 'general', + i18n: 'settings.settings.general', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + i18n: 'settings.settings.general.enableFeature42' + } + ] + }, + { + type: 'section', + name: 'copyOfGeneral', + i18n: 'settings.settings.copyOfGeneral', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + default: false, + i18n: 'settings.settings.general.enableFeature42' + } + ] + }, + { + type: 'section', + name: 'aSection', + i18n: 'settings.settings.aSection', + content: [ + { + type: 'bool', + name: 'Enable feature 43', + i18n: 'settings.settings.aSection.enableFeature43' + }, + { + type: 'selection', + name: 'language', + i18n: 'settings.settings.aSection.language.label', + default: 'en', + options: [ + { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, + { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, + ] + }, + { + type: 'section', + name: 'section2', + i18n: 'settings.settings.aSection.section2.label', + content: [ + { + type: 'string', + name: 'string', + i18n: 'settings.settings.aSection.sections.string', + default: 'str' + } + ] + } + ] + } + ] + } }, + { settings: { + content: [ + { + type: 'section', + name: 'general', + i18n: 'settings.settings.general', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + i18n: 'settings.settings.general.enableFeature42' + } + ] + }, + { + type: 'section', + name: 'copyOfGeneral', + i18n: 'settings.settings.copyOfGeneral', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + default: false, + i18n: 'settings.settings.general.enableFeature42' + } + ] + }, + { + type: 'section', + name: 'aSection', + i18n: 'settings.settings.aSection', + content: [ + { + type: 'bool', + name: 'Enable feature 43', + i18n: 'settings.settings.aSection.enableFeature43' + }, + { + type: 'selection', + name: 'language', + i18n: 'settings.settings.aSection.language.label', + default: 'en', + options: [ + { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, + { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, + ] + }, + { + type: 'section', + name: 'section2', + i18n: 'settings.settings.aSection.section2.label', + content: [ + { + type: 'string', + name: 'string', + i18n: 'settings.settings.aSection.sections.string', + default: 'str' + } + ] + } + ] + } + ] + } }, + { settings: { + contents: [ + { + type: 'section', + name: 'general', + i18n: 'settings.settings.general', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + i18n: 'settings.settings.general.enableFeature42' + } + ] + }, + { + type: 'section', + name: 'copyOfGeneral', + i18n: 'settings.settings.copyOfGeneral', + content: [ + { + type: 'bool', + name: 'Enable feature 42', + default: false, + i18n: 'settings.settings.general.enableFeature42' + } + ] + }, + { + type: 'section', + name: 'aSection', + i18n: 'settings.settings.aSection', + content: [ + { + type: 'bool', + name: 'Enable feature 43', + i18n: 'settings.settings.aSection.enableFeature43', + default: 'true' + }, + { + type: 'selection', + name: 'language', + i18n: 'settings.settings.aSection.language.label', + default: 'en', + options: [ + { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, + { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, + ] + }, + { + type: 'section', + name: 'section2', + i18n: 'settings.settings.aSection.section2.label', + content: [ + { + type: 'string', + name: 'string', + i18n: 'settings.settings.aSection.sections.string', + default: 42 + } + ] + } + ] + } + ] + } }, + { settings: { contents: [ { type: 'bool', @@ -258,276 +521,14 @@ describe('settingsValidator', () => { default: 'str' } ] - }, + } ] - }, + } ] - }, expected: false }, - { raw: { - contents: [ - { - type: 'section', - name: 'general', - i18n: 'settings.settings.general', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'copyOfGeneral', - i18n: 'settings.settings.copyOfGeneral', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - default: false, - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'aSection', - i18n: 'settings.settings.aSection', - content: [ - { - type: 'bool', - name: 'Enable feature 43', - i18n: 'settings.settings.aSection.enableFeature43', - default: 'true' - }, - { - type: 'selection', - name: 'language', - i18n: 'settings.settings.aSection.language.label', - default: 'en', - options: [ - { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, - { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, - ] - }, - { - type: 'section', - name: 'section2', - i18n: 'settings.settings.aSection.section2.label', - content: [ - { - type: 'string', - name: 'string', - i18n: 'settings.settings.aSection.sections.string', - default: 42 - } - ] - }, - ] - }, - ] - }, expected: false }, - { raw: { - content: [ - { - type: 'section', - name: 'general', - i18n: 'settings.settings.general', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'copyOfGeneral', - i18n: 'settings.settings.copyOfGeneral', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - default: false, - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'aSection', - i18n: 'settings.settings.aSection', - content: [ - { - type: 'bool', - name: 'Enable feature 43', - i18n: 'settings.settings.aSection.enableFeature43' - }, - { - type: 'selection', - name: 'language', - i18n: 'settings.settings.aSection.language.label', - default: 'en', - options: [ - { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, - { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, - ] - }, - { - type: 'section', - name: 'section2', - i18n: 'settings.settings.aSection.section2.label', - content: [ - { - type: 'string', - name: 'string', - i18n: 'settings.settings.aSection.sections.string', - default: 'str' - } - ] - }, - ] - }, - ] - }, expected: false }, - { raw: { - content: [ - { - type: 'sectio', - name: 'general', - i18n: 'settings.settings.general', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'copyOfGeneral', - i18n: 'settings.settings.copyOfGeneral', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - default: false, - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'aSection', - i18n: 'settings.settings.aSection', - content: [ - { - type: 'bool', - name: 'Enable feature 43', - i18n: 'settings.settings.aSection.enableFeature43' - }, - { - type: 'selection', - name: 'language', - i18n: 'settings.settings.aSection.language.label', - default: 'en', - options: [ - { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, - { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, - ] - }, - { - type: 'section', - name: 'section2', - i18n: 'settings.settings.aSection.section2.label', - content: [ - { - type: 'string', - name: 'string', - i18n: 'settings.settings.aSection.sections.string', - default: 'str' - } - ] - }, - ] - }, - ] - }, expected: false }, - { raw: { - content: [ - { - type: 'sectio', - name: 'general', - i18n: 'settings.settings.general', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'copyOfGeneral', - i18n: 'settings.settings.copyOfGeneral', - content: [ - { - type: 'bool', - name: 'Enable feature 42', - default: false, - i18n: 'settings.settings.general.enableFeature42' - } - ] - }, - { - type: 'section', - name: 'aSection', - i18n: 'settings.settings.aSection', - content: [ - { - type: 'bool', - name: 'Enable feature 43', - i18n: 'settings.settings.aSection.enableFeature43' - }, - { - type: 'selection', - name: 'language', - i18n: 'settings.settings.aSection.language.label', - default: 'en', - allowMultiple: 'false', - options: [ - { name: 'en', i18n: 'settings.settings.aSection.language.options.en' }, - { name: 'de', i18n: 'settings.settings.aSection.language.options.de' }, - ] - }, - { - type: 'section', - name: 'section2', - i18n: 'settings.settings.aSection.section2.label', - content: [ - { - type: 'string', - name: 'string', - i18n: 'settings.settings.aSection.sections.string', - default: 'str' - } - ] - }, - ] - }, - ] - }, expected: false } - ])('returns valid: $expected', ({ raw, expected }) => { - const result = validateSettingsConfig(raw); - if (!result.valid) { - console.error('Error message:', result.error); - }; - expect(result.valid).toBe(expected); - }); + } } + ])('throws error', ({ settings }) => { + expect(() => validateSettingsConfig(settings)).toThrow(Error) + }) }); describe('validateEntry', () => { diff --git a/src/features/settings/utils/settingsParser.js b/src/features/settings/utils/settingsParser.js deleted file mode 100644 index 1dd9626..0000000 --- a/src/features/settings/utils/settingsParser.js +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2026 Seekra - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { useSettingsConfigCacheStore } from '../stores/settingsConfigCacheStore'; -import { validateSettingsConfig } from './settingsValidator.js'; - -/** - * Loads and parses the settings configuration via dynamic import. - * @returns {Promise} - */ -export async function loadSettingsConfig() { - const settingsConfigCache = useSettingsConfigCacheStore(); - let raw; - - try { - raw = (await import('../settings.json')).default; - settingsConfigCache.set(raw); - } catch (e) { - throw new Error(`[settings] Failed to load settings.json: ${e.message}`); - } - - const result = validateSettingsConfig(raw); - if (!result.valid) { - throw new Error(result.error); - } - - return result.config; -} \ No newline at end of file diff --git a/src/features/settings/utils/settingsValidator.js b/src/features/settings/utils/settingsValidator.js index a54daa7..ebe1554 100644 --- a/src/features/settings/utils/settingsValidator.js +++ b/src/features/settings/utils/settingsValidator.js @@ -117,18 +117,13 @@ export const validateFirstLevelSection = function validateFirstLevelSection (sec * @returns {{ valid: true, config: import('../types/settingsConfig').SettingsConfig } | { valid: false, error: string }} */ export function validateSettingsConfig(raw) { - try { - if (!raw || typeof raw !== 'object') { - throw new Error('[settings] Config must be an object'); - } - if (!Array.isArray(raw.contents)) { - throw new Error('[settings] "contents" must be an array'); - } - raw.contents.forEach((entry, i) => - validateFirstLevelSection(entry, `contents[${i}]`) - ); - return { valid: true, config: raw }; - } catch (e) { - return { valid: false, error: e.message }; + if (!raw || typeof raw !== 'object') { + throw new Error('[settings] Config must be an object'); } + if (!Array.isArray(raw.contents)) { + throw new Error('[settings] "contents" must be an array'); + } + raw.contents.forEach((entry, i) => + validateFirstLevelSection(entry, `contents[${i}]`) + ); } \ No newline at end of file diff --git a/src/features/settings/views/SettingsView.vue b/src/features/settings/views/SettingsView.vue index 57936ed..d241ee9 100644 --- a/src/features/settings/views/SettingsView.vue +++ b/src/features/settings/views/SettingsView.vue @@ -18,9 +18,9 @@ limitations under the License. import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue'; import SettingsPage from '../components/SettingsPage.vue'; +import settingsConfiguration from '../settings.json'; import { getSettingRecursively } from '../utils/getSetting.js'; -import { loadSettingsConfig } from '../utils/settingsParser'; -import { onMounted, ref, watchEffect } from 'vue'; +import { computed, onMounted, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; @@ -28,39 +28,27 @@ const { t } = useI18n(); const route = useRoute(); const router = useRouter(); -const settingsLoaded = ref(false) -const settings = ref([]); -const activeSectionContent = ref({}); -const activeSection = ref(null); +const settings = settingsConfiguration.contents; -watchEffect(() => { +const activePath = computed(() => { const segments = route.path .split('/') .filter(Boolean); - activeSection.value = segments.slice(1, segments.length).join('.'); + return segments.slice(1, segments.length); }); -const updateSettings = function updateSettings () { - if (activeSection.value) { - const setting = getSettingRecursively(activeSection.value.split('.'), settings.value); - if (!setting) { - router.push('/settings'); - } else { - activeSectionContent.value = setting; - }; +const activeSection = ref(null); +const updateSettings = async function updateSettings () { + const setting = getSettingRecursively(activePath.value, settings); + if (setting) { + activeSection.value = setting; } else { - activeSectionContent.value = { content: [] }; - }; + await router.push('/settings'); + } }; - -onMounted(async () => { - settings.value = (await loadSettingsConfig()).contents; - watchEffect(() => { - updateSettings(); - }); - settingsLoaded.value = true; -}); +watch(route, (newRoute) => updateSettings(newRoute)); +onMounted(updateSettings); const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSidebarFunction) { if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction(); @@ -81,7 +69,7 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi {{ t(section.i18n) }} @@ -89,12 +77,9 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi -
- {{ t('loading') }} -
-
+
- +
diff --git a/src/features/settings/views/__tests__/SettingsView.test.js b/src/features/settings/views/__tests__/SettingsView.test.js index e8584ce..446f3a1 100644 --- a/src/features/settings/views/__tests__/SettingsView.test.js +++ b/src/features/settings/views/__tests__/SettingsView.test.js @@ -16,8 +16,8 @@ limitations under the License. import SettingsView from '../SettingsView.vue'; import { mountComponent } from '@/test-utils/mountComponent.js'; -import { flushPromises } from '@vue/test-utils'; import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { nextTick } from 'vue'; const settingsHoisted = vi.hoisted(() => [ { @@ -197,10 +197,8 @@ const settings = settingsHoisted.values(); const originalCurrentSection = 's0'; let currentSection = originalCurrentSection; -vi.mock('../../utils/settingsParser', () => ({ - loadSettingsConfig: vi.fn().mockResolvedValue({ - contents: settingsHoisted - }) +vi.mock('../../settings.json', () => ({ + default: { contents: settingsHoisted } })); vi.mock('vue-router', async (importOriginal) => { @@ -235,9 +233,7 @@ describe('SettingsView', () => { describe('sidebar', () => { test('shows all top-level sections', async () => { const wrapper = getWrapper(); - - await flushPromises(); - await wrapper.vm.$nextTick(); + await nextTick(); const sidebarItems = wrapper.findAll('.sidebar-sections-list > li'); settings.forEach((setting, index) => { @@ -254,47 +250,27 @@ describe('SettingsView', () => { describe('main', () => { describe('correct element', () => { - test('shows that it is loading', () => { - const wrapper = getWrapper(); - - const mainContentLoading = wrapper.find('.main-content--loading'); - const mainContentLoaded = wrapper.find('.main-content--loaded'); - const mainContentNoSection = wrapper.find('.main-content--no-section'); - - expect(mainContentLoading.exists()).toBe(true); - expect(mainContentLoaded.exists()).toBe(false); - expect(mainContentNoSection.exists()).toBe(false); - }); - test('shows that no section is selected', async () => { currentSection = ''; const wrapper = getWrapper(); + await nextTick(); - await flushPromises(); - await wrapper.vm.$nextTick(); - - const mainContentLoading = wrapper.find('.main-content--loading'); - const mainContentLoaded = wrapper.find('.main-content--loaded'); + const mainContent = wrapper.find('.settings-main-content'); const mainContentNoSection = wrapper.find('.main-content--no-section'); - expect(mainContentLoading.exists()).toBe(false); - expect(mainContentLoaded.exists()).toBe(false); + expect(mainContent.exists()).toBe(false); expect(mainContentNoSection.exists()).toBe(true); }); test('shows main content', async () => { const wrapper = getWrapper(); + await nextTick(); - await flushPromises(); - await wrapper.vm.$nextTick(); - - const mainContentLoading = wrapper.find('.main-content--loading'); - const mainContentLoaded = wrapper.find('.main-content--loaded'); + const mainContent = wrapper.find('.settings-main-content'); const mainContentNoSection = wrapper.find('.main-content--no-section'); - expect(mainContentLoading.exists()).toBe(false); - expect(mainContentLoaded.exists()).toBe(true); + expect(mainContent.exists()).toBe(true); expect(mainContentNoSection.exists()).toBe(false); }); }); diff --git a/src/main.js b/src/main.js index 90efd54..d7efce6 100644 --- a/src/main.js +++ b/src/main.js @@ -30,6 +30,9 @@ import rawFonts from './styles/fonts.json'; import './styles/common.css'; import './styles/variables/colors.css'; +import settings from './features/settings/settings.json'; +import { validateSettingsConfig } from './features/settings/utils/settingsValidator'; + (async () => { const fonts = rawFonts.map((font) => new FontFace( font.family ?? '', @@ -54,6 +57,8 @@ import './styles/variables/colors.css'; const pinia = createPinia(); pinia.use(piniaPluginPersistedstate); + + validateSettingsConfig(settings); createApp(App) .use(router) -- 2.39.5 From ad6f59a85c2c6c8230044bdeabd2c68fec868e01 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 6 Aug 2026 19:49:12 +0200 Subject: [PATCH 3/6] fix(settings): fix useSettings getSetting store path --- .../composables/__tests__/useSettings.test.js | 16 ++++++---------- src/features/settings/composables/useSettings.js | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/features/settings/composables/__tests__/useSettings.test.js b/src/features/settings/composables/__tests__/useSettings.test.js index ddcdba8..aec200d 100644 --- a/src/features/settings/composables/__tests__/useSettings.test.js +++ b/src/features/settings/composables/__tests__/useSettings.test.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useSettingsStore } from '../../stores/settingsStore'; import { useSettings } from '../useSettings'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; @@ -50,31 +49,28 @@ vi.mock('../../settings.json', () => ({ describe('useSettings', () => { describe('getSetting', () => { test('returns stored value', () => { - const { getSetting } = useSettings(); - const settings = useSettingsStore(); + const { getSetting, setSetting } = useSettings(); - const key = 'setting'; + const key = ['setting']; const value = 42; - settings.set(key, value); - + setSetting(key, value); expect(getSetting(key)).toBe(value); }); test('returns default value', () => { - const { getSetting } = useSettings(); + const { getSetting, setSetting } = useSettings(); expect(getSetting(settingPath)).toBe(settingDefaultValue); }); test('returns stored value instead of default value', () => { - const { getSetting } = useSettings(); - const settings = useSettingsStore(); + const { getSetting, setSetting } = useSettings(); const value = 43; expect(getSetting(settingPath)).toBe(settingDefaultValue); - settings.set(settingPath, value); + setSetting(settingPath, value); expect(getSetting(settingPath)).toBe(value); }); diff --git a/src/features/settings/composables/useSettings.js b/src/features/settings/composables/useSettings.js index e87e839..8c469a6 100644 --- a/src/features/settings/composables/useSettings.js +++ b/src/features/settings/composables/useSettings.js @@ -31,7 +31,7 @@ export const useSettings = function useSettings () { const settingsStore = useSettingsStore(); return ( - settingsStore.get(key) ?? + settingsStore.get(key.join('.')) ?? getSettingRecursively( key, settings.contents ?? [] )?.default -- 2.39.5 From 287f02505e6b5628e1cdedbf6abdfeb6a80b03ee Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 6 Aug 2026 19:54:21 +0200 Subject: [PATCH 4/6] feat(color-scheme): make color scheme a setting The color scheme now uses settings to store the currect color scheme. --- src/App.vue | 22 ++++++++++--------- .../components/ColorSchemeButton.vue | 12 +++++++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/App.vue b/src/App.vue index 6728627..c6d080c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,34 +19,36 @@ import Navbar from './features/nav/components/Navbar.vue'; import Footer from './features/footer/components/Footer.vue'; import { updatePageTitle } from './router'; -import { useColorScheme } from './features/colorScheme/composables/useColorScheme'; -import { ref, provide, watch, watchEffect } from 'vue'; +import { useSettings } from './features/settings/composables/useSettings.js'; +import { computed, watch, watchEffect } from 'vue'; import { useRoute } from 'vue-router'; const route = useRoute(); -const { getColorScheme, updateColorScheme } = useColorScheme(); -const colorScheme = ref(null); -provide('colorScheme', colorScheme); -watch(colorScheme, (newValue) => { - updateColorScheme(newValue); +const { getSetting } = useSettings(); +const colorScheme = computed(() => getSetting(['appearance', 'colorScheme'])); + +const updateColorScheme = function updateColorScheme (newColorScheme) { document.body.style.setProperty(colorScheme, { auto: 'normal', dark: 'dark', light: 'light' }); - if (newValue === 'dark') { + if (newColorScheme === 'dark') { document.body.classList.add('dark'); } else { document.body.classList.remove('dark'); } - if (newValue === 'auto') { + if (newColorScheme === 'auto') { document.body.classList.add('color-scheme-auto'); } else { document.body.classList.remove('color-scheme-auto'); } +} +watch(colorScheme, (newColorScheme) => { + updateColorScheme(newColorScheme); }); -colorScheme.value = getColorScheme(); +updateColorScheme(colorScheme.value); watchEffect(() => updatePageTitle(route)); diff --git a/src/features/colorScheme/components/ColorSchemeButton.vue b/src/features/colorScheme/components/ColorSchemeButton.vue index 4f4c228..ed56e9d 100644 --- a/src/features/colorScheme/components/ColorSchemeButton.vue +++ b/src/features/colorScheme/components/ColorSchemeButton.vue @@ -17,12 +17,14 @@ limitations under the License.