14 Commits

22 changed files with 92 additions and 3215 deletions
+2 -1071
View File
File diff suppressed because it is too large Load Diff
-15
View File
@@ -1,15 +0,0 @@
# Changelog
## [Unreleased]
### Added
- Start page
- Settings page
- Footer
- Navbar
- Searchbar
- Icons
- Internationalization and some major languages
- Color scheme
- Search results view with error message which indicates that the search is not available
+1 -836
View File
File diff suppressed because it is too large Load Diff
+3 -8
View File
@@ -6,9 +6,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview"
"test": "vitest",
"test:run": "vitest run"
}, },
"dependencies": { "dependencies": {
"terser": "^5.47.1", "terser": "^5.47.1",
@@ -17,11 +15,8 @@
"vue-router": "^5.0.6" "vue-router": "^5.0.6"
}, },
"devDependencies": { "devDependencies": {
"@vue/test-utils": "^2.4.6",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.6",
"jsdom": "^29.1.1",
"vite": "^8.0.10", "vite": "^8.0.10",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2"
"vitest": "^4.1.7"
} }
} }
+2 -11
View File
@@ -42,22 +42,13 @@ watch(colorScheme, val => updateColorScheme(val))
</template> </template>
<style scoped> <style scoped>
#app-wrapper { .main-content {
--main-content-padding-x: 30px; --main-content-padding-x: 30px;
--main-content-padding-y: 40px; --main-content-padding-y: 40px;
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x); --main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x) 0;
}
.main-content {
flex-grow: 1; flex-grow: 1;
} }
@media (max-width: 48rem) {
#app-wrapper {
--main-content-padding-x: 15px;
}
}
#app-wrapper { #app-wrapper {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
-36
View File
@@ -1,36 +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.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
stroke="currentColor"
stroke-width="3"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
d="M2 22 l8 -8"
/>
<circle
stroke="currentColor"
stroke-width="3"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
cx="15.5"
cy="8.5"
r="6.5"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

+8 -7
View File
@@ -15,8 +15,6 @@ limitations under the License.
--> -->
<script setup> <script setup>
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
@@ -38,10 +36,11 @@ const copyrightPeriod =
<RouterLink to="settings" class="link"> <RouterLink to="settings" class="link">
{{ t('preferences.settings') }} {{ t('preferences.settings') }}
</RouterLink> </RouterLink>
<LanguageSwitchButton />
</div> </div>
<div class="footer-segment"> <div class="footer-segment">
&copy; {{ copyrightPeriod }} Seekra <div class="copyright-note">
&copy; {{ copyrightPeriod }} Seekra
</div>
</div> </div>
</footer> </footer>
</template> </template>
@@ -52,9 +51,6 @@ const copyrightPeriod =
} }
.footer-segment { .footer-segment {
display: flex;
justify-content: center;
gap: 32px;
padding: var(--padding-y); padding: var(--padding-y);
background-color: var(--light-bg); background-color: var(--light-bg);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
@@ -63,4 +59,9 @@ const copyrightPeriod =
.global-footer a { .global-footer a {
color: var(--dark); color: var(--dark);
} }
.copyright-note {
display: flex;
justify-content: center;
}
</style> </style>
@@ -71,15 +71,7 @@ const open = function () {
</span> </span>
</button> </button>
<ul <ul v-if="isOpen" ref="languageDropdown" class="language-dropdown" role="listbox">
v-if="isOpen"
ref="languageDropdown"
class="language-dropdown"
role="listbox"
:style="{
bottom: 'calc(var(--offset) + 2 * var(--trigger-padding-y) + 1em)' // easier to add auto adaption to the available space
}"
>
<li <li
v-for="lang in SUPPORTED_LANGUAGES" v-for="lang in SUPPORTED_LANGUAGES"
:key="lang" :key="lang"
@@ -96,7 +88,6 @@ const open = function () {
<style scoped> <style scoped>
.language-switch { .language-switch {
--trigger-padding-y: 4px;
position: relative; position: relative;
} }
@@ -107,7 +98,7 @@ const open = function () {
background: none; background: none;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
padding: var(--trigger-padding-y) 10px; padding: 4px 10px;
cursor: pointer; cursor: pointer;
color: var(--dark); color: var(--dark);
} }
@@ -127,9 +118,9 @@ const open = function () {
} }
.language-dropdown { .language-dropdown {
--offset: 6px;
position: absolute; position: absolute;
right: 0; right: 0;
top: calc(100% + 6px);
background-color: var(--light-bg); background-color: var(--light-bg);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
@@ -1,114 +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 { expect, test, describe, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
import { loadLanguage } from '@/i18n';
vi.mock('@/i18n', () => ({
loadLanguage: vi.fn(() => Promise.resolve()),
LANGUAGES_RTL: ['ar', 'he'],
SUPPORTED_LANGUAGES: ['en', 'de', 'ar']
}));
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
locale: { value: 'de' }
})
}));
vi.mock('@/features/icons/components/Icon.vue', () => ({
default: {
name: 'Icon',
template: '<span>Icon</span>'
}
}));
describe('LanguageSwitchButton.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.lang = '';
document.documentElement.dir = '';
});
test('renders correctly with initial state closed', () => {
const wrapper = mount(LanguageSwitchButton);
expect(wrapper.find('.language-button').exists()).toBe(true);
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
expect(wrapper.find('.language-button').attributes('aria-expanded')).toBe('false');
});
test('opens the dropdown when language button is clicked', async () => {
const wrapper = mount(LanguageSwitchButton);
const button = wrapper.find('.language-button');
await button.trigger('click');
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
expect(button.attributes('aria-expanded')).toBe('true');
});
const languageTestCases = [
{ code: 'en', expectedDir: 'ltr' },
{ code: 'de', expectedDir: 'ltr' },
{ code: 'ar', expectedDir: 'rtl' }
];
test.for(languageTestCases)('selectLanguage($code) sets localStorage, html attributes and changes layout direction to $expectedDir', async ({ code, expectedDir }) => {
const wrapper = mount(LanguageSwitchButton);
await wrapper.find('.language-button').trigger('click');
const options = wrapper.findAll('.language-dropdown li');
const optionToClick = options.find(opt => opt.text().includes(code));
await optionToClick.trigger('click');
expect(loadLanguage).toHaveBeenCalledWith(code);
expect(localStorage.getItem('locale')).toBe(code);
expect(document.documentElement.lang).toBe(code);
expect(document.documentElement.dir).toBe(expectedDir);
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
});
test('closes the dropdown when clicking outside the component', async () => {
const wrapper = mount(LanguageSwitchButton, {
attachTo: document.body
});
await wrapper.find('.language-button').trigger('click');
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
await new Promise(resolve => setTimeout(resolve, 0));
const externalDiv = document.createElement('div');
document.body.appendChild(externalDiv);
const clickEvent = new MouseEvent('click', { bubbles: true });
externalDiv.dispatchEvent(clickEvent);
await wrapper.vm.$nextTick();
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
wrapper.unmount();
externalDiv.remove();
});
});
+29 -28
View File
@@ -15,9 +15,25 @@ limitations under the License.
--> -->
<script setup> <script setup>
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue'; import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
import logo from '@/assets/images/logo.svg'; import logo from '@/assets/images/logo.svg';
import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue'; import Searchbar from '@/features/search/components/Searchbar.vue';
const route = useRoute();
const searchQueryModel = defineModel();
watch(() => route.name, name => {
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
});
watch(() => route.query.q, q => {
if (route.name === 'searchResults') {
searchQueryModel.value = q || '';
}
});
</script> </script>
<template> <template>
@@ -25,16 +41,21 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
<RouterLink to="/" class="link button link"> <RouterLink to="/" class="link button link">
<img :src="logo" alt="Seekra" class="nav-logo" /> <img :src="logo" alt="Seekra" class="nav-logo" />
</RouterLink> </RouterLink>
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper" /> <Searchbar
v-if="route.name === 'searchResults'"
class="search-bar"
v-model="searchQueryModel"
auto-submit
/>
<ul class="right-links"> <ul class="right-links">
<li>
<LanguageSwitchButton />
</li>
<li> <li>
<ColorSchemeButton /> <ColorSchemeButton />
</li> </li>
</ul> </ul>
</nav> </nav>
<div class="navbar-search-bar-wrapper-small-screens-wrapper">
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper-small-screens" />
</div>
</template> </template>
<style scoped> <style scoped>
@@ -42,7 +63,7 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 18px var(--main-content-padding-x); padding: 18px 40px;
height: 42px; height: 42px;
} }
@@ -68,27 +89,7 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
height: 24px; height: 24px;
width: auto; width: auto;
} }
.search-bar {
.navbar-search-bar-wrapper { width: 70%;
width: 100%;
padding: 0 30px;
}
.navbar-search-bar-wrapper-small-screens-wrapper {
padding: 0 15px 15px;
}
.navbar-search-bar-wrapper-small-screens {
display: none;
}
@media (max-width: 48rem) {
.navbar-search-bar-wrapper {
display: none;
}
.navbar-search-bar-wrapper-small-screens {
display: block;
}
} }
</style> </style>
@@ -1,43 +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.
-->
<script setup>
import Searchbar from '@/features/search/components/Searchbar.vue';
import { watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const searchQueryModel = defineModel();
watch(() => route.name, name => {
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
});
watch(() => route.query.q, q => {
if (route.name === 'searchResults') {
searchQueryModel.value = q || '';
}
});
</script>
<template>
<Searchbar
v-if="route.name === 'searchResults'"
v-model="searchQueryModel"
auto-submit
/>
</template>
+3 -26
View File
@@ -15,8 +15,6 @@ limitations under the License.
--> -->
<script setup> <script setup>
import Icon from '@/features/icons/components/Icon.vue';
const searchQuery = defineModel(); const searchQuery = defineModel();
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -39,7 +37,7 @@ const submitSearch = function () {
<template> <template>
<div> <div>
<form @submit.prevent="submitSearch" class="search-form"> <form @submit.prevent="submitSearch">
<div class="search-wrapper"> <div class="search-wrapper">
<input <input
v-model="searchQuery" v-model="searchQuery"
@@ -47,14 +45,7 @@ const submitSearch = function () {
:placeholder="t('search.searchBar.placeholder')" :placeholder="t('search.searchBar.placeholder')"
required required
/> />
<button <button type="submit" class="search-button">{{ t('search.searchBar.submit') }}</button>
type="submit"
class="search-button"
:title="t('search.searchBar.submit')"
:aria-label="t('search.searchBar.submit')"
>
<Icon class="search-icon" name="magnifying-glass" size="1.1em" />
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -73,8 +64,7 @@ const submitSearch = function () {
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding)); border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding));
padding: var(--padding); padding: var(--padding);
padding-left: var(--padding-left); padding-left: var(--padding-left);
width: 100%; width: calc(100% - var(--padding-left));
box-sizing: border-box;
} }
.search-wrapper input { .search-wrapper input {
@@ -84,13 +74,11 @@ const submitSearch = function () {
font-size: 1rem; font-size: 1rem;
background: transparent; background: transparent;
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y)); height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
padding-left: 12px;
} }
.search-button { .search-button {
font-size: 1rem; font-size: 1rem;
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y)); height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
width: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y)); border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y));
border: none; border: none;
padding: var(--submit-button-padding-y) 20px; padding: var(--submit-button-padding-y) 20px;
@@ -98,20 +86,9 @@ const submitSearch = function () {
color: var(--white); color: var(--white);
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
}
.search-button .search-icon {
filter: invert(1);
} }
.search-button:hover { .search-button:hover {
background: var(--primary-color-l-1); background: var(--primary-color-l-1);
} }
.search-form {
width: 100%;
}
</style> </style>
@@ -53,7 +53,9 @@ const { t } = useI18n();
--error-message-padding: 2em; --error-message-padding: 2em;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%; width: calc(100% - 2 * var(--main-content-padding-x));
position: absolute;
top: calc(50vh - 0.5 * var(--error-message-height) - var(--error-message-padding));
} }
.search-results-error-message { .search-results-error-message {
@@ -53,6 +53,10 @@ limitations under the License.
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry * @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
*/ */
/**
* @typedef {Object} SettingsConfig
* @property {SettingConfigEntry[]} contents
*/
/** /**
* @typedef {{ type: 'bool', name: string, i18n: string, description?: string, default: boolean }} BoolSettingConfig * @typedef {{ type: 'bool', name: string, i18n: string, description?: string, default: boolean }} BoolSettingConfig
* @typedef {{ type: 'number', name: string, i18n: string, description?: string, default: number }} NumberSettingConfig * @typedef {{ type: 'number', name: string, i18n: string, description?: string, default: number }} NumberSettingConfig
@@ -60,6 +64,5 @@ limitations under the License.
* @typedef {{ type: 'selection', name: string, i18n: string, description?: string, default: string | string[], allowMultiple?: boolean, options: SelectionOption[] }} SelectionSettingConfig * @typedef {{ type: 'selection', name: string, i18n: string, description?: string, default: string | string[], allowMultiple?: boolean, options: SelectionOption[] }} SelectionSettingConfig
* @typedef {{ type: 'section', name: string, i18n?: string, description?: string, content: SettingConfigEntry[] }} SectionSettingConfig * @typedef {{ type: 'section', name: string, i18n?: string, description?: string, content: SettingConfigEntry[] }} SectionSettingConfig
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry * @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
* @typedef {{ contents: SettingConfigEntry[] }} FirstLevelSettingConfigEntry * @typedef {{ contents: SettingConfigEntry[] }} SettingsConfig
* @typedef {{ contents: FirstLevelSettingConfigEntry[] }} SettingsConfig
*/ */
@@ -1,758 +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 { describe, test, expect } from 'vitest';
import { validateSettingsConfig, validateEntry, validateSelectionOptions, assertType, assertString } from '../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: {
contents: []
}, expected: true },
{ raw: {
contents: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
}
]
}, expected: true },
{ raw: {
contents: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
]
}, expected: true },
{ raw: {
contents: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
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: true },
{ raw: {
contents: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
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: {
contents: [
{
type: 'bool',
name: 'aStandaloneBooleanSetting',
i18n: 'settings.settings.aStandaloneBooleanSetting',
default: true
},
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
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 },
{ 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);
});
});
describe('validateEntry', () => {
test.for([
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: false }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 42 }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 0 }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: -42.7 }],
[{ name: 'aString', type: 'string', i18n: 'aString.label' }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: '' }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: 'Seekra is great!' }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', options: [
{ name: 'yes', i18n: 'selectSomething.options.yes' },
{ name: 'no', i18n: 'selectSomething.options.no' },
{ name: 'maybe', i18n: 'selectSomething.options.maybe' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'no', options: [
{ name: 'yes', i18n: 'selectSomething.options.yes' },
{ name: 'no', i18n: 'selectSomething.options.no' },
{ name: 'maybe', i18n: 'selectSomething.options.maybe' }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true },
{ name: 'aSecondSection', type: 'section', i18n: 'sections.aSecondSection.title', content: [
{ name: 'enableFeature44', type: 'bool', i18n: 'feature.44.enable', default: true },
] }
] }]
])('throws no error for the entry %s', ([ entry ]) => {
expect(() => validateEntry(entry)).not.throws(Error);
});
test.for([
[{ name: 'enableFeature42', type: 'bool', i18n: '' }],
[{ name: 'enableFeature42', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: 'feature.42.enable' }],
[{ name: '', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: 'feature.42.enable' }],
[{ name: '', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: '' }],
[{ name: '', type: 'bool', i18n: '' }],
[{ name: '', type: 'bool', i18n: '' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 42 }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: '42' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'Seekra' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'true' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'false' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'undefined' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'null' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: [] }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: {} }],
[{ name: 'enableFeature42', type: '', i18n: 'feature.42.enable', default: {} }],
[{ name: '', type: 'number', i18n: 'aNumber.label' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: '42' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'zero' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'undefined' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'false' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: true }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: false }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: [] }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: {} }],
[{ name: 'aNumber', type: '', i18n: 'aNumber.label', default: {} }],
[{ name: 'aNumber', type: '', i18n: 'aNumber.label', default: 42 }],
[{ name: 'aString', type: 'string', i18n: '' }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: 42 }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: true }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: false }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: [] }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: {} }],
[{ name: 'aString', type: 'string', i18n: '', default: {} }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: true }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: [] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'c', options: [
{ name: 'a', i18n: 'a' },
{ name: 'b', i18n: 'b' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: [
{ name: 'a', i18n: 'a' },
{ name: 'b', i18n: '' },
{ name: 'c' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', allowMultiple: 'false', options: [
{ name: 'a', i18n: 'a' },
{ name: 'b', i18n: '' },
{ name: 'c' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'no', options: [
{ name: 'yes', i18n: '' },
{ name: '', i18n: 'selectSomething.options.no' },
{ name: 'maybe', i18n: 'selectSomething.options.maybe' },
{ name: '', i18n: '' },
{ name: 42, i18n: 43 },
{ name: '' },
{ i18n: '' },
{}
] }],
[{ name: 'aSection', type: 'section', i18n: '', content: [] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: '' }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: '[]' }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: 'a' }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.42.enable', default: 0 },
{ name: 'enableFeature44', type: 'bool' }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true },
{ name: 'aSecondSection', type: 'section', i18n: 'sections.aSecondSection.title', content: 'Hello' },
{ name: 'aSecondSection', type: 'section', i18n: '', content: [] },
{ name: 'aSecondSection', type: 'section', i18n: 'i18n', content: [] },
{ name: 'aSecondSection', type: 'section', content: [] }
] }]
])('throws an error for the entry %s', ([ entry ]) => {
expect(() => validateEntry(entry)).throws(Error);
});
});
describe('validateSelectionOptions', () => {
test.for([
[[{ name: 'test', i18n: 'test.label' }]],
[[{ name: 'test', i18n: 'test.label' }, { name: 'test2', i18n: 'test2.label' }]],
[[{ name: 'test', i18n: 'test.label' }, { name: 'test', i18n: 'test2.label' }, { name: 'test3', i18n: 'test.label' }]]
])('throws no error for the options %s', ([ options ]) => {
expect(() => validateSelectionOptions(options)).not.throws(Error);
});
test.for([
[[{}]],
[[{ i18n: '' }]],
[[{ i18n: 'a' }]],
[[{ name: 'a' }]],
[[{ name: '' }]],
[[{ name: '', i18n: 'test.label' }]],
[[{ name: 'test', i18n: '' }]],
[[{ name: '', i18n: '' }]],
[[{ name: '', i18n: 'test.label' }, { name: 'test2', i18n: '' }]],
[[{ name: 'test', i18n: '' }, { name: '', i18n: 'test2.label' }, { name: '', i18n: ' ' }, { name: '42', i18n: '42.i18n' }]],
[[]]
])('throws an error for the options %s', ([ options ]) => {
expect(() => validateSelectionOptions(options)).throws(Error);
});
});
describe('assertType', () => {
test.for([
['bool'],
['number'],
['string'],
['selection'],
['section']
])('throws no error for the value %s', ([ value ]) => {
expect(() => assertType(value)).not.throw(Error);
});
test.for([
[''],
[' '],
[' '],
[' '],
['42'],
['0'],
['-42'],
['-42.0'],
['-0.0'],
['a'],
['ab'],
['SeekraIsGreat!'],
['Seekra is great!'],
[undefined],
[null]
])('throws an error for the value %s', ([ value ]) => {
expect(() => assertType(value)).throw(Error);
});
});
describe('assertString', () => {
test.for([
['a'],
['b'],
['ab'],
['0'],
['42'],
['null'],
['undefined'],
['()&%())']
])('throws no error for the value %s', ([ value ]) => {
expect(() => assertString(value)).not.throw(Error);
});
test.for([
[0],
[1],
[42],
[-1],
[-42],
[''],
[' '],
[' '],
[' ']
])('throws an error for the value %s', ([ value ]) => {
expect(() => assertString(value)).throws(Error);
});
});
@@ -16,13 +16,13 @@ limitations under the License.
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section']; const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
export const assertString = function assertString (value, path) { function assertString(value, path) {
if (typeof value !== 'string' || value.trim() === '') { if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`[settings] "${path}" must be a non-empty string`); throw new Error(`[settings] "${path}" must be a non-empty string`);
} }
} }
export const assertType = function assertType (value, path) { function assertType(value, path) {
if (!VALID_TYPES.includes(value)) { if (!VALID_TYPES.includes(value)) {
throw new Error( throw new Error(
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}` `[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`
@@ -30,23 +30,21 @@ export const assertType = function assertType (value, path) {
} }
} }
export const validateSelectionOptions = function validateSelectionOptions (options, path) { function validateSelectionOptions(options, path) {
if (!Array.isArray(options) || options.length === 0) { if (!Array.isArray(options) || options.length === 0) {
throw new Error(`[settings] "${path}.options" must be a non-empty array`); throw new Error(`[settings] "${path}.options" must be a non-empty array`);
}; }
options.forEach((opt, i) => { options.forEach((opt, i) => {
assertString(opt.name, `${path}.options[${i}].name`); assertString(opt.name, `${path}.options[${i}].name`);
assertString(opt.i18n, `${path}.options[${i}].i18n`); assertString(opt.i18n, `${path}.options[${i}].i18n`);
}); });
} }
export const validateEntry = function validateEntry (entry, path) { function validateEntry(entry, path) {
assertType(entry.type, `${path}.type`); assertType(entry.type, `${path}.type`);
assertString(entry.name, `${path}.name`);
assertString(entry.i18n, `${path}.i18n`);
if (entry.type === 'section') { if (entry.type === 'section') {
assertString(entry.name, `${path}.name`);
if (!Array.isArray(entry.content)) { if (!Array.isArray(entry.content)) {
throw new Error(`[settings] "${path}.content" must be an array`); throw new Error(`[settings] "${path}.content" must be an array`);
} }
@@ -55,47 +53,29 @@ export const validateEntry = function validateEntry (entry, path) {
); );
return; return;
} }
if (entry.type === 'selection') { assertString(entry.name, `${path}.name`);
validateSelectionOptions(entry.options, path); assertString(entry.i18n, `${path}.i18n`);
if (typeof entry.allowMultiple !== 'boolean' && entry.allowMultiple) {
throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`);
}
};
if (entry.default !== undefined) { if (entry.default !== undefined) {
if (entry.type === 'bool' && typeof entry.default !== 'boolean') { if (entry.type === 'bool' && typeof entry.default !== 'boolean') {
throw new Error(`[settings] "${path}.default" must be a boolean`); throw new Error(`[settings] "${path}.default" must be a boolean`);
} }
if (entry.type === 'number' && typeof entry.default !== 'number') { if (entry.type === 'number' && typeof entry.default !== 'number') {
throw new Error(`[settings] "${path}.default" must be a number`); throw new Error(`[settings] "${path}.default" must be a number`);
} }
if (entry.type === 'string' && typeof entry.default !== 'string') { if (entry.type === 'string' && typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`); throw new Error(`[settings] "${path}.default" must be a string`);
} }
if (entry.type === 'selection') { if (entry.type === 'selection') {
if (typeof entry.default !== 'string') { validateSelectionOptions(entry.options, path);
throw new Error(`[settings] "${path}.default" must be a string`); if (typeof entry.allowMultiple !== 'boolean') {
}; throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`);
if (!entry.options.map((option) => option.name).includes(entry.default)) { }
throw new Error(`[settings] option "${path}.default" does not exist`); }
};
};
} }
} }
export const validateFirstLevelSection = function validateFirstLevelSection (section, path) {
assertString(section.name);
assertString(section.i18n);
if (!Array.isArray(section.content)) {
throw new Error(`[settings] "${path}.content" must be an array`);
};
section.content.forEach((entry, i) =>
validateEntry(entry, `${path}.content[${i}]`)
);
};
/** /**
* Validates a raw settings config object. * Validates a raw settings config object.
* @param {unknown} raw * @param {unknown} raw
@@ -110,7 +90,7 @@ export function validateSettingsConfig(raw) {
throw new Error('[settings] "contents" must be an array'); throw new Error('[settings] "contents" must be an array');
} }
raw.contents.forEach((entry, i) => raw.contents.forEach((entry, i) =>
validateFirstLevelSection(entry, `contents[${i}]`) validateEntry(entry, `contents[${i}]`)
); );
return { valid: true, config: raw }; return { valid: true, config: raw };
} catch (e) { } catch (e) {
+5 -8
View File
@@ -16,18 +16,15 @@ limitations under the License.
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import { i18n, loadLanguage } from './i18n' import { i18n, loadLanguage } from './i18n';
import getCurrentLanguage from './utils/currentLanguage' import getCurrentLanguage from './utils/currentLanguage';
import router from './router' import router from './router'
import './styles/common.css' import './styles/common.css'
import './styles/variables/colors.css' import './styles/variables/colors.css'
(async () => { await loadLanguage(getCurrentLanguage());
await loadLanguage(getCurrentLanguage())
createApp(App) createApp(App)
.use(router) .use(router)
.use(i18n) .use(i18n)
.mount('#app') .mount('#app')
})()
-122
View File
@@ -1,122 +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 { expect, test } from 'vitest';
import { ensureUnit } from '../cssDimensions';
test.for([
{ dimension: null, expected: '0px' },
{ dimension: undefined, expected: '0px' },
{ dimension: 0, expected: '0px' },
{ dimension: -0, expected: '0px' },
{ dimension: -1, expected: '-1px' },
{ dimension: -42, expected: '-42px' },
{ dimension: 1, expected: '1px' },
{ dimension: 42, expected: '42px' },
{ dimension: 0.0, expected: '0px' },
{ dimension: -0.0, expected: '0px' },
{ dimension: -1.0, expected: '-1px' },
{ dimension: -42.0, expected: '-42px' },
{ dimension: 1.0, expected: '1px' },
{ dimension: 42.0, expected: '42px' },
{ dimension: 0.42, expected: '0.42px' },
{ dimension: -0.42, expected: '-0.42px' },
{ dimension: -1.42, expected: '-1.42px' },
{ dimension: -42.42, expected: '-42.42px' },
{ dimension: 1.42, expected: '1.42px' },
{ dimension: 42.42, expected: '42.42px' },
{ dimension: '0', expected: '0px' },
{ dimension: '-0', expected: '-0px' },
{ dimension: '-1', expected: '-1px' },
{ dimension: '-42', expected: '-42px' },
{ dimension: '1', expected: '1px' },
{ dimension: '42', expected: '42px' },
{ dimension: '0.0', expected: '0.0px' },
{ dimension: '-0.0', expected: '-0.0px' },
{ dimension: '-1.0', expected: '-1.0px' },
{ dimension: '-42.0', expected: '-42.0px' },
{ dimension: '1.0', expected: '1.0px' },
{ dimension: '42.0', expected: '42.0px' },
{ dimension: '0.42', expected: '0.42px' },
{ dimension: '-0.42', expected: '-0.42px' },
{ dimension: '-1.42', expected: '-1.42px' },
{ dimension: '-42.42', expected: '-42.42px' },
{ dimension: '1.42', expected: '1.42px' },
{ dimension: '42.42', expected: '42.42px' },
{ dimension: '0px', expected: '0px' },
{ dimension: '-0px', expected: '-0px' },
{ dimension: '-1px', expected: '-1px' },
{ dimension: '-42px', expected: '-42px' },
{ dimension: '1px', expected: '1px' },
{ dimension: '42px', expected: '42px' },
{ dimension: '0.0px', expected: '0.0px' },
{ dimension: '-0.0px', expected: '-0.0px' },
{ dimension: '-1.0px', expected: '-1.0px' },
{ dimension: '-42.0px', expected: '-42.0px' },
{ dimension: '1.0px', expected: '1.0px' },
{ dimension: '42.0px', expected: '42.0px' },
{ dimension: '0.42px', expected: '0.42px' },
{ dimension: '-0.42px', expected: '-0.42px' },
{ dimension: '-1.42px', expected: '-1.42px' },
{ dimension: '-42.42px', expected: '-42.42px' },
{ dimension: '1.42px', expected: '1.42px' },
{ dimension: '42.42px', expected: '42.42px' },
{ dimension: '0em', expected: '0em' },
{ dimension: '-0em', expected: '-0em' },
{ dimension: '-1em', expected: '-1em' },
{ dimension: '-42em', expected: '-42em' },
{ dimension: '1em', expected: '1em' },
{ dimension: '42em', expected: '42em' },
{ dimension: '0.0em', expected: '0.0em' },
{ dimension: '-0.0em', expected: '-0.0em' },
{ dimension: '-1.0em', expected: '-1.0em' },
{ dimension: '-42.0em', expected: '-42.0em' },
{ dimension: '1.0em', expected: '1.0em' },
{ dimension: '42.0em', expected: '42.0em' },
{ dimension: '0.42em', expected: '0.42em' },
{ dimension: '-0.42em', expected: '-0.42em' },
{ dimension: '-1.42em', expected: '-1.42em' },
{ dimension: '-42.42em', expected: '-42.42em' },
{ dimension: '1.42em', expected: '1.42em' },
{ dimension: '42.42em', expected: '42.42em' },
{ dimension: 'calc(42px - 1em)', expected: 'calc(42px - 1em)' },
{ dimension: 'calc(42px)', expected: 'calc(42px)' },
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
{ dimension: 'max(42px, 5rem)', expected: 'max(42px, 5rem)' },
{ dimension: 'clamp(42vh, 23vw, 13cap)', expected: 'clamp(42vh, 23vw, 13cap)' },
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
{ dimension: 'max(42px,5rem)', expected: 'max(42px,5rem)' },
{ dimension: 'clamp(42vh,23vw,13cap)', expected: 'clamp(42vh,23vw,13cap)' },
{ dimension: 'clamp( 42vh,23vw,13cap )', expected: 'clamp( 42vh,23vw,13cap )' }
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
expect(ensureUnit(dimension)).toBe(expected);
});
@@ -1,61 +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 { expect, test } from 'vitest';
import getCurrentLanguage from '../currentLanguage';
const locales = [
{ navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'}
];
test.for(locales)('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
if (localStorageLanguage) {
localStorage.setItem('locale', localStorageLanguage);
};
expect(getCurrentLanguage()).toBe(expected);
});
+1 -1
View File
@@ -43,7 +43,7 @@ const { t } = useI18n();
.error-message{ .error-message{
margin: 0; margin: 0;
font-weight: 600; font-weight: 600;
font-size: 1.75rem; font-size: 2vw;
} }
#link { #link {
align-items: center; align-items: center;
+2 -3
View File
@@ -67,10 +67,9 @@ const submitSearch = function () {
max-width: 100%; max-width: 100%;
} }
.slogan { .slogan{
margin-top: 1rem; margin: 0;
font-size: small; font-size: small;
line-height: normal;
} }
.search-container { .search-container {
-7
View File
@@ -21,9 +21,6 @@ import { createHtmlPlugin } from 'vite-plugin-html';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
build: {
target: 'es2020'
},
plugins: [ plugins: [
vue(), vue(),
createHtmlPlugin({ createHtmlPlugin({
@@ -34,9 +31,5 @@ export default defineConfig({
alias: { alias: {
'@': path.resolve(__dirname, './src') '@': path.resolve(__dirname, './src')
} }
},
test: {
globals: false,
environment: 'jsdom'
} }
}) })