Style: make code style consistent #186

Merged
jakob.scheid merged 29 commits from style/make-code-style-consistent into main 2026-08-09 13:09:54 +02:00
91 changed files with 14258 additions and 6033 deletions
+1 -1
View File
@@ -42,4 +42,4 @@ jobs:
printf "Update current build pointer ... "
echo "$BUILD_ID" | mc pipe "minio/$BUCKET/current"
echo "done"
echo "done"
+1 -1
View File
@@ -1,3 +1,3 @@
.vscode/
node_modules/
dist/
dist/
+3
View File
@@ -0,0 +1,3 @@
.gitea/
.github/
eslint.config.js
+4
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"tabWidth": 4
}
+1 -1
View File
@@ -5,4 +5,4 @@ This software is provided "as is", without warranty of any kind.
This software uses the Vue.js framework.
This software makes use of various open-source components.
See THIRD_PARTY_LICENSES for details.
See THIRD_PARTY_LICENSES for details.
+1 -1
View File
@@ -8,4 +8,4 @@ Please execute the setup script in `scripts/` for your platform when cloning the
## Legal
After each update to the dependencies, please run the script `scripts/generate-license-files.js` to regenerate the third-party license files.
After each update to the dependencies, please run the script `scripts/generate-license-files.js` to regenerate the third-party license files.
+181 -9
View File
@@ -213,6 +213,30 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
-----------
The following npm package may be included in this product:
- rfdc@1.4.1
This package contains the following license:
Copyright 2019 "David Mark Clements <david.mark.clements@gmail.com>"
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
-----------
The following npm package may be included in this product:
- @jridgewell/resolve-uri@3.1.2
@@ -560,6 +584,67 @@ SOFTWARE.
-----------
The following npm packages may be included in this product:
- copy-anything@4.0.5
- is-what@5.5.0
These packages each contain the following license:
MIT License
Copyright (c) 2018 Luca Ban - Mesqueeb
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- superjson@2.2.6
This package contains the following license:
MIT License
Copyright (c) 2020 Simon Knott and superjson contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm packages may be included in this product:
- birpc@2.9.0
@@ -591,6 +676,36 @@ SOFTWARE.
-----------
The following npm package may be included in this product:
- mitt@3.0.1
This package contains the following license:
MIT License
Copyright (c) 2021 Jason Miller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- unplugin@3.0.0
@@ -683,9 +798,12 @@ SOFTWARE.
The following npm packages may be included in this product:
- @vue/devtools-api@8.1.1
- @vue/devtools-kit@8.1.1
- @vue/devtools-shared@8.1.1
- @vue/devtools-api@7.7.10
- @vue/devtools-api@8.1.5
- @vue/devtools-kit@7.7.10
- @vue/devtools-kit@8.1.5
- @vue/devtools-shared@7.7.10
- @vue/devtools-shared@8.1.5
These packages each contain the following license:
@@ -715,7 +833,7 @@ SOFTWARE.
The following npm package may be included in this product:
- tinyglobby@0.2.16
- tinyglobby@0.2.17
This package contains the following license:
@@ -775,7 +893,9 @@ SOFTWARE.
The following npm packages may be included in this product:
- defu@6.1.7
- mlly@1.8.2
- perfect-debounce@1.0.0
- perfect-debounce@2.1.0
- scule@1.3.0
- ufo@1.6.4
@@ -1222,6 +1342,57 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
-----------
The following npm package may be included in this product:
- pinia-plugin-persistedstate@4.7.1
This package contains the following license:
MIT License
Copyright (c) 2021 praz <https://codeberg.org/praz>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- speakingurl@14.0.1
This package contains the following license:
The BSD 3-Clause License (BSD3)
Copyright (c) 2013-2017 Sascha Droste <pid@posteo.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----------
The following npm package may be included in this product:
- chokidar@5.0.0
@@ -1284,7 +1455,7 @@ SOFTWARE.
The following npm package may be included in this product:
- picomatch@4.0.4
- picomatch@4.0.5
This package contains the following license:
@@ -1351,11 +1522,12 @@ THE SOFTWARE.
-----------
The following npm package may be included in this product:
The following npm packages may be included in this product:
- pinia@3.0.4
- vue-router@5.0.6
This package contains the following license:
These packages each contain the following license:
The MIT License (MIT)
@@ -1587,7 +1759,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- postcss@8.5.14
- postcss@8.5.26
This package contains the following license:
@@ -1616,7 +1788,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product:
- nanoid@3.3.12
- nanoid@3.3.18
This package contains the following license:
+4062 -23
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,4 +13,4 @@
- Internationalization and some major languages
- Color scheme
- Search results view with error message which indicates that the search is not available
- Settings renderer
- Settings renderer
+50
View File
@@ -0,0 +1,50 @@
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import pluginVue from 'eslint-plugin-vue';
import globals from 'globals';
export default defineConfig([
{
files: ['**/*.{js,mjs,cjs,vue}'],
plugins: { js },
extends: ['js/recommended'],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
rules: {
'eqeqeq': 'error',
'no-unused-vars': 'error',
'no-undef': 'error',
'prefer-const': 'error',
'no-var': 'error',
'no-duplicate-imports': 'error',
'no-import-assign': 'error',
'no-empty-function': 'error',
'default-param-last': 'error',
'prefer-arrow-callback': 'error',
'require-await': 'warn',
'no-async-promise-executor': 'error',
'no-unreachable': 'error',
'no-constant-condition': 'error',
'no-debugger': 'error',
'no-alert': 'error',
'no-console': 'warn',
'no-new-object': 'error',
'no-array-constructor': 'error',
'no-prototype-builtins': 'error',
'object-shorthand': 'error',
'prefer-template': 'error',
},
},
pluginVue.configs['flat/essential'],
]);
+18 -8
View File
@@ -24,7 +24,19 @@ limitations under the License.
<noscript>
<style>
body {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-family:
'Inter',
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
}
a {
@@ -40,17 +52,15 @@ limitations under the License.
</head>
<body>
<noscript>
<h1>
Seekra
</h1>
<p>
You need to enable JavaScript to use Seekra.
</p>
<h1>Seekra</h1>
<p>You need to enable JavaScript to use Seekra.</p>
<section>
<h2>Legal Content</h2>
<ul>
<li>
<a href="https://assets.seekra.jcloud-services.ddns.net/2026-08-04-76a067e/legal/legal_notice.html">
<a
href="https://assets.seekra.jcloud-services.ddns.net/2026-08-04-76a067e/legal/legal_notice.html"
>
Legal Notice
</a>
</li>
+5 -5
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
}
}
+6957 -3857
View File
File diff suppressed because it is too large Load Diff
+38 -28
View File
@@ -1,30 +1,40 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run"
},
"dependencies": {
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"terser": "^5.47.1",
"vue": "^3.5.32",
"vue-i18n": "^11.4.4",
"vue-router": "^5.0.6"
},
"devDependencies": {
"@pinia/testing": "^1.0.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/test-utils": "^2.4.6",
"jsdom": "^29.1.1",
"vite": "^8.0.10",
"vite-plugin-html": "^3.2.2",
"vitest": "^4.1.7"
}
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier . --write",
"format:check": "prettier . --check"
},
"dependencies": {
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"terser": "^5.47.1",
"vue": "^3.5.32",
"vue-i18n": "^11.4.4",
"vue-router": "^5.0.6"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@pinia/testing": "^1.0.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/test-utils": "^2.4.6",
"eslint": "^10.8.1",
"eslint-plugin-vue": "^10.10.0",
"generate-license-file": "^4.2.1",
"globals": "^17.9.0",
"jsdom": "^29.1.1",
"prettier": "^3.9.6",
"vite": "^8.0.10",
"vite-plugin-html": "^3.2.2",
"vitest": "^4.1.7"
}
}
+6 -17
View File
@@ -22,21 +22,16 @@ import { generateLicenseFile } from 'generate-license-file';
const packageJson = './package.json';
const packageJsonBackup = `${packageJson}~`;
await generateLicenseFile(
packageJson,
'THIRD_PARTY_LICENSES'
);
await generateLicenseFile(packageJson, 'THIRD_PARTY_LICENSES');
const pkg = JSON.parse(
await fs.readFile(packageJson, 'utf-8')
);
const pkg = JSON.parse(await fs.readFile(packageJson, 'utf-8'));
await fs.rename(packageJson, packageJsonBackup);
const devPackage = {
...pkg,
dependencies: pkg.devDependencies ?? {},
devDependencies: {}
devDependencies: {},
};
/*
@@ -45,14 +40,8 @@ const devPackage = {
https://github.com/TobyAndToby/generate-license-file/issues/752
*/
await fs.writeFile(
packageJson,
JSON.stringify(devPackage)
);
await fs.writeFile(packageJson, JSON.stringify(devPackage));
await generateLicenseFile(
packageJson,
'./THIRD_PARTY_LICENSES_DEV'
);
await generateLicenseFile(packageJson, './THIRD_PARTY_LICENSES_DEV');
await fs.rename(packageJsonBackup, packageJson);
await fs.rename(packageJsonBackup, packageJson);
+1 -1
View File
@@ -14,4 +14,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
git config --local commit.template .gitmessage
git config --local commit.template .gitmessage
+1 -1
View File
@@ -14,4 +14,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
git config --local commit.template .gitmessage
git config --local commit.template .gitmessage
+11 -11
View File
@@ -15,13 +15,12 @@ limitations under the License.
-->
<script setup>
import Navbar from './features/nav/components/Navbar.vue';
import Footer from './features/footer/components/Footer.vue';
import { LANGUAGE_PATH } from './config/featureSettings.js';
import { updatePageTitle } from './router';
import AppFooter from './features/footer/components/AppFooter.vue';
import AppNavbar from './features/nav/components/AppNavbar.vue';
import { useSettings } from './features/settings/composables/useSettings.js';
import { setLocale } from './i18n';
import { updatePageTitle } from './router';
import { computed, onMounted, watch, watchEffect } from 'vue';
import { useRoute } from 'vue-router';
@@ -30,11 +29,11 @@ const route = useRoute();
const { getSetting } = useSettings();
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
const updateColorScheme = function updateColorScheme (newColorScheme) {
const updateColorScheme = function updateColorScheme(newColorScheme) {
document.body.style.setProperty(colorScheme, {
auto: 'normal',
dark: 'dark',
light: 'light'
light: 'light',
});
if (newColorScheme === 'dark') {
document.body.classList.add('dark');
@@ -46,7 +45,7 @@ const updateColorScheme = function updateColorScheme (newColorScheme) {
} else {
document.body.classList.remove('color-scheme-auto');
}
}
};
watch(colorScheme, (newColorScheme) => {
updateColorScheme(newColorScheme);
});
@@ -61,11 +60,11 @@ onMounted(() => {
<template>
<div id="app-wrapper">
<Navbar />
<AppNavbar />
<router-view class="main-content" />
<Footer />
<AppFooter />
</div>
</template>
@@ -73,7 +72,8 @@ onMounted(() => {
#app-wrapper {
--main-content-padding-x: 30px;
--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);
min-height: 100vh;
display: flex;
flex-direction: column;
@@ -90,4 +90,4 @@ onMounted(() => {
--main-content-padding-x: 15px;
}
}
</style>
</style>
+1 -1
View File
@@ -23,4 +23,4 @@ limitations under the License.
stroke-linejoin="round"
d="M5 12 l6 8 l8 -16"
/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 832 B

After

Width:  |  Height:  |  Size: 833 B

+1 -1
View File
@@ -23,4 +23,4 @@ limitations under the License.
stroke-linejoin="round"
d="M4 8 l8 8 l8 -8"
/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 831 B

+1 -1
View File
@@ -23,4 +23,4 @@ limitations under the License.
stroke-linejoin="round"
d="M8 20 l8 -8 l-8 -8"
/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 834 B

+1 -1
View File
@@ -35,4 +35,4 @@ limitations under the License.
a9 9 0 0 0 -9 -9
"
/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -33,4 +33,4 @@ limitations under the License.
c-2.140625 4.351562000000001 -6.480468999999999 7.058593000000002 -11.328125 7.058593000000002
"
/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -33,4 +33,4 @@ limitations under the License.
cy="8.5"
r="6.5"
/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -35,4 +35,4 @@ limitations under the License.
v16
"
/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -42,4 +42,4 @@ limitations under the License.
<!-- bottom right -->
<circle fill="currentColor" r="2" cx="18.36396103067893" cy="18.36396103067893" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+1 -1
View File
@@ -16,4 +16,4 @@ limitations under the License.
<svg xmlns="http://www.w3.org/2000/svg" width="758.75" height="192.5">
<defs><linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" style="stop-color:#4ba8eb"/><stop offset="100%" style="stop-color:#043485"/></linearGradient></defs>
<path stroke="url(#gradient)" stroke-width="15" fill="none" d=" M94 51 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 1 43.75 43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 M138.75 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M270 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M401.25 0 v192.5 m0 -96.125 l87.5 -87.5 m-87.5 87.5 l87.5 87.5 M532.5 192.5 v-192.5 m0 51.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 M663.75 50.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v43.75 v43.75 v43.75 m0 -43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v50"/></svg>
<path stroke="url(#gradient)" stroke-width="15" fill="none" d=" M94 51 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 1 43.75 43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 M138.75 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M270 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M401.25 0 v192.5 m0 -96.125 l87.5 -87.5 m-87.5 87.5 l87.5 87.5 M532.5 192.5 v-192.5 m0 51.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 M663.75 50.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v43.75 v43.75 v43.75 m0 -43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v50"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+2 -2
View File
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export const getAssetsUrl = function getAssetsUrl () {
export const getAssetsUrl = function getAssetsUrl() {
return new URL(
`${import.meta.env.VITE_ASSETS_BUILD_ID}/`,
import.meta.env.VITE_ASSETS_BASE_URL
import.meta.env.VITE_ASSETS_BASE_URL,
).href;
};
@@ -15,8 +15,7 @@ limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import UIIcon from '@/features/icons/components/UIIcon.vue';
import { useSettings } from '@/features/settings/composables/useSettings';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
@@ -27,33 +26,36 @@ const { getSetting, setSetting } = useSettings();
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
const colorSchemeNextMapper = {
'light': 'dark',
'dark': 'auto',
'auto': 'light'
light: 'dark',
dark: 'auto',
auto: 'light',
};
const colorSchemeIconMapper = {
'dark': 'crescent-moon',
'light': 'sun',
'auto': 'circle-black-white'
dark: 'crescent-moon',
light: 'sun',
auto: 'circle-black-white',
};
const getTooltipTranslation = function (colorScheme) {
return t(`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`);
const getTooltipTranslation = function getTooltipTranslation(colorScheme) {
return t(
`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`,
);
};
const setColorScheme = function setColorScheme (newColorScheme) {
const setColorScheme = function setColorScheme(newColorScheme) {
setSetting(['appearance', 'colorScheme'], newColorScheme);
};
</script>
<template>
<button class="color-scheme-button"
<button
class="color-scheme-button"
@click="setColorScheme(colorSchemeNextMapper[colorScheme])"
:aria-label="getTooltipTranslation(colorScheme)"
:title="getTooltipTranslation(colorScheme)"
>
<Icon
<UIIcon
:name="colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]]"
size="16"
/>
@@ -78,4 +80,4 @@ const setColorScheme = function setColorScheme (newColorScheme) {
.color-scheme-button:hover {
background: var(--light-hover);
}
</style>
</style>
@@ -21,9 +21,9 @@ import { describe, expect, test } from 'vitest';
const { getSetting } = useSettings();
const colorSchemeSettingPath = ['appearance', 'colorScheme']
const colorSchemeSettingPath = ['appearance', 'colorScheme'];
const getWrapper = function getWrapper () {
const getWrapper = function getWrapper() {
return mountComponent(ColorSchemeButton);
};
@@ -32,7 +32,7 @@ describe('ColorSchemeButton', () => {
const wrapper = getWrapper();
expect(getSetting(colorSchemeSettingPath)).toBe('auto');
const colorSchemeButton = wrapper.find('button');
await colorSchemeButton.trigger('click');
@@ -16,20 +16,17 @@ limitations under the License.
<script setup>
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const startYear = 2026
const currentYear = new Date().getFullYear()
const startYear = 2026;
const currentYear = new Date().getFullYear();
const copyrightPeriod =
startYear === currentYear
? `${currentYear}`
: `${startYear}-${currentYear}`
: `${startYear}-${currentYear}`;
</script>
<template>
@@ -103,4 +100,4 @@ const copyrightPeriod =
margin-top: 0.8em;
}
}
</style>
</style>
@@ -15,13 +15,12 @@ limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import { LANGUAGE_PATH } from '@/config/featureSettings';
import UIIcon from '@/features/icons/components/UIIcon.vue';
import { useSettings } from '@/features/settings/composables/useSettings';
import { setLocale, SUPPORTED_LANGUAGES } from '@/i18n';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { setLocale, SUPPORTED_LANGUAGES } from '@/i18n';
const { t, locale } = useI18n();
const { setSetting, getSetting } = useSettings();
@@ -29,33 +28,33 @@ const { setSetting, getSetting } = useSettings();
const isOpen = ref(false);
const languageDropdown = ref(null);
const selectLanguage = function selectLanguage (code) {
const selectLanguage = function selectLanguage(code) {
setSetting(LANGUAGE_PATH, code);
close();
};
watch(() => getSetting(LANGUAGE_PATH), setLocale);
const close = function () {
document.removeEventListener('click', closeWrapperOnClickOutsite);
const close = function close() {
document.removeEventListener('click', closeWrapperOnClickOutside);
isOpen.value = false;
};
const closeWrapperOnClickOutsite = function (e) {
const closeWrapperOnClickOutside = function closeWrapperOnClickOutside(e) {
if (languageDropdown.value) {
if (!languageDropdown.value.contains(e.target)) {
close();
};
};
}
}
};
const open = function () {
const open = function open() {
if (!isOpen.value) {
isOpen.value = true;
setTimeout(() => {
document.addEventListener('click', closeWrapperOnClickOutsite);
document.addEventListener('click', closeWrapperOnClickOutside);
}, 0);
};
}
};
</script>
@@ -67,9 +66,11 @@ const open = function () {
:aria-expanded="isOpen"
aria-haspopup="listbox"
>
<span class="lang-code">{{ t(`preferences.locale.languages.${locale}`) }}</span>
<span class="lang-code">{{
t(`preferences.locale.languages.${locale}`)
}}</span>
<span class="chevron" :class="{ open: isOpen }">
<Icon name="chevron-down" size="1em" />
<UIIcon name="chevron-down" size="1em" />
</span>
</button>
@@ -79,7 +80,7 @@ const open = function () {
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
bottom: 'calc(var(--offset) + 2 * var(--trigger-padding-y) + 1em)', // easier to add auto adaption to the available space
}"
>
<li
@@ -90,7 +91,9 @@ const open = function () {
:class="{ active: lang === locale }"
@click="selectLanguage(lang)"
>
<span class="lang-label">{{ t(`preferences.locale.languages.${lang}`) }}</span>
<span class="lang-label">{{
t(`preferences.locale.languages.${lang}`)
}}</span>
</li>
</ul>
</div>
@@ -163,4 +166,4 @@ const open = function () {
.flag {
font-size: 1.1rem;
}
</style>
</style>
@@ -14,31 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
import { LANGUAGE_PATH } from '@/config/featureSettings.js';
import { useSettings } from '@/features/settings/composables/useSettings.js';
import { mountComponent } from '@/test-utils/mountComponent.js';
import { expect, test, describe } from 'vitest';
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
import { describe, expect, test } from 'vitest';
const getWrapper = function getWrapper () {
const getWrapper = function getWrapper() {
return mountComponent(LanguageSwitchButton);
};
describe('LanguageSwitchButton', () => {
test('renders correctly with initial state closed', () => {
const wrapper = getWrapper();
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');
expect(
wrapper.find('.language-button').attributes('aria-expanded'),
).toBe('false');
});
test('opens the dropdown when language button is clicked', async () => {
const wrapper = getWrapper();
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');
});
@@ -47,17 +49,17 @@ describe('LanguageSwitchButton', () => {
const { getSetting } = useSettings();
const wrapper = getWrapper();
await wrapper.find('.language-button').trigger('click');
const options = wrapper.findAll('.language-dropdown li');
const enOption = options.find(opt => opt.text().includes('en'));
const enOption = options.find((opt) => opt.text().includes('en'));
await enOption.trigger('click');
expect(getSetting(LANGUAGE_PATH)).toBe('en');
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
const deOption = options.find(opt => opt.text().includes('de'));
const deOption = options.find((opt) => opt.text().includes('de'));
await deOption.trigger('click');
expect(getSetting(LANGUAGE_PATH)).toBe('de');
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
@@ -69,14 +71,14 @@ describe('LanguageSwitchButton', () => {
await wrapper.find('.language-button').trigger('click');
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
await new Promise(resolve => setTimeout(resolve, 0));
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);
@@ -84,4 +86,4 @@ describe('LanguageSwitchButton', () => {
wrapper.unmount();
externalDiv.remove();
});
});
});
@@ -19,33 +19,36 @@ import { describe, expect, test } from 'vitest';
describe('navigatorLanguage', () => {
test.for([
{ navigatorLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en', expected: 'en' },
{ navigatorLanguage: 'de', expected: 'de' },
{ navigatorLanguage: 'fr', expected: 'fr' },
{ navigatorLanguage: 'en-US', expected: 'en'},
{ navigatorLanguage: 'en-AU', expected: 'en'},
{ navigatorLanguage: 'de-DE', expected: 'de'},
{ navigatorLanguage: 'fr-FR', expected: 'fr'},
{ navigatorLanguage: 'en-US', expected: 'en' },
{ navigatorLanguage: 'en-AU', expected: 'en' },
{ navigatorLanguage: 'de-DE', expected: 'de' },
{ navigatorLanguage: 'fr-FR', expected: 'fr' },
{ navigatorLanguage: 'en-us', expected: 'en'},
{ navigatorLanguage: 'en-au', expected: 'en'},
{ navigatorLanguage: 'de-de', expected: 'de'},
{ navigatorLanguage: 'fr-fr', expected: 'fr'},
{ navigatorLanguage: 'en-us', expected: 'en' },
{ navigatorLanguage: 'en-au', expected: 'en' },
{ navigatorLanguage: 'de-de', expected: 'de' },
{ navigatorLanguage: 'fr-fr', expected: 'fr' },
{ navigatorLanguage: 'zh-Hans-CN', expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', expected: 'uz'},
{ navigatorLanguage: 'zh-Hans-CN', expected: 'zh' },
{ navigatorLanguage: 'zh-Hant-TW', expected: 'zh' },
{ navigatorLanguage: 'uz-Latn-UZ', expected: 'uz' },
{ navigatorLanguage: 'en-US-u-ca-gregory', expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', expected: 'zh'}
])('returns language $expected with navigator language $navigatorLanguage', ({ navigatorLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
{ navigatorLanguage: 'en-US-u-ca-gregory', expected: 'en' },
{ navigatorLanguage: 'de-DE-u-co-phonebk', expected: 'de' },
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', expected: 'zh' },
])(
'returns language $expected with navigator language $navigatorLanguage',
({ navigatorLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true,
});
expect(getNavigatorLanguage()).toBe(expected);
});
expect(getNavigatorLanguage()).toBe(expected);
},
);
});
+2 -2
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export const getNavigatorLanguage = function getNavigatorLanguage () {
export const getNavigatorLanguage = function getNavigatorLanguage() {
const locale = new Intl.Locale(navigator.language);
return locale.language;
};
};
@@ -15,33 +15,33 @@ limitations under the License.
-->
<script setup>
import { computed } from 'vue';
import { ensureUnit } from '@/utils/cssDimensions';
import { computed } from 'vue';
const props = defineProps({
name: {
required: true,
type: String
type: String,
},
size: {
type: [Number, String],
default: 24
}
})
default: 24,
},
});
const icons = import.meta.glob('@/assets/icons/*.svg', {
eager: true,
import: 'default'
})
import: 'default',
});
const Icon = computed(() => icons[`/src/assets/icons/${props.name}.svg`])
const icon = computed(() => icons[`/src/assets/icons/${props.name}.svg`]);
const size = computed(() => ensureUnit(props.size))
const size = computed(() => ensureUnit(props.size));
</script>
<template>
<div class="icon-container">
<img :src="Icon" :style="{ height: `${size}`, width: `${size}` }" />
<img :src="icon" :style="{ height: `${size}`, width: `${size}` }" />
</div>
</template>
@@ -51,4 +51,4 @@ const size = computed(() => ensureUnit(props.size))
align-items: center;
filter: var(--invert);
}
</style>
</style>
+3 -3
View File
@@ -15,8 +15,8 @@ limitations under the License.
*/
export class FetchError extends Error {
constructor (message) {
constructor(message) {
super(message);
this.name = 'FetchError';
};
};
}
}
@@ -60,29 +60,25 @@ describe('fetchLegalContent', () => {
describe('getLegalNotice', () => {
describe('success', () => {
test('returns HTML body', async () => {
fetchSpy.mockResolvedValue(
new Response(exampleHtmlDocument)
);
fetchSpy.mockResolvedValue(new Response(exampleHtmlDocument));
expect(
(await getLegalNotice()).trim()
).toBe(exampleHtmlWithLinkClass);
expect((await getLegalNotice()).trim()).toBe(
exampleHtmlWithLinkClass,
);
});
test('returns HTML <main>', async () => {
fetchSpy.mockResolvedValue(
new Response(exampleHtmlDocumentWithMain)
new Response(exampleHtmlDocumentWithMain),
);
expect(
(await getLegalNotice()).trim()
).toBe(exampleHtmlWithLinkClass);
expect((await getLegalNotice()).trim()).toBe(
exampleHtmlWithLinkClass,
);
});
test('does not fail on success status code', async () => {
fetchSpy.mockResolvedValue(
new Response(null, { status: 206 })
);
fetchSpy.mockResolvedValue(new Response(null, { status: 206 }));
await getLegalNotice();
});
@@ -95,9 +91,7 @@ describe('fetchLegalContent', () => {
});
test('throws FetchError on HTTP error', async () => {
fetchSpy.mockResolvedValue(
new Response(null, { status: 404 })
)
fetchSpy.mockResolvedValue(new Response(null, { status: 404 }));
await expect(getLegalNotice()).rejects.toThrow(FetchError);
});
@@ -17,7 +17,7 @@ limitations under the License.
import { FetchError } from '../errors';
import { getAssetsUrl } from '@/config/env';
const fetchHtml = async function fetchHtml (url, options) {
const fetchHtml = async function fetchHtml(url, options) {
let response;
try {
response = await fetch(url, options);
@@ -29,16 +29,17 @@ const fetchHtml = async function fetchHtml (url, options) {
else throw new FetchError();
};
export const getLegalNotice = async function getLegalNotice () {
const legalNoticeUrl = new URL('legal/legal_notice.html', getAssetsUrl()).href;
export const getLegalNotice = async function getLegalNotice() {
const legalNoticeUrl = new URL('legal/legal_notice.html', getAssetsUrl())
.href;
const html = await fetchHtml(legalNoticeUrl);
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const legalNoticeContainer = doc.querySelector('main') ?? doc.body;
legalNoticeContainer.querySelectorAll('a').forEach(
(link) => link.classList.add('link')
);
legalNoticeContainer
.querySelectorAll('a')
.forEach((link) => link.classList.add('link'));
return legalNoticeContainer.innerHTML;
};
@@ -20,16 +20,18 @@ import { mountComponent } from '@/test-utils/mountComponent';
import { flushPromises } from '@vue/test-utils';
import { afterEach, describe, expect, test, vi } from 'vitest';
const getLegalNotice = vi.hoisted(() => vi.fn().mockResolvedValue('legal notice'));
const getLegalNotice = vi.hoisted(() =>
vi.fn().mockResolvedValue('legal notice'),
);
vi.mock('../../services/fetchLegalContent', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
getLegalNotice
getLegalNotice,
};
});
afterEach(() => getLegalNotice.mockRestore());
afterEach(getLegalNotice.mockRestore);
describe('LegalNoticeView', () => {
test('shows loading indicator', () => {
@@ -15,9 +15,9 @@ limitations under the License.
-->
<script setup>
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
import logo from '@/assets/images/logo.svg';
import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
import logo from '@/assets/images/logo.svg';
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
</script>
<template>
@@ -33,7 +33,9 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
</ul>
</nav>
<div class="navbar-search-bar-wrapper-small-screens-wrapper">
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper-small-screens" />
<NavbarSearchBarWrapper
class="navbar-search-bar-wrapper-small-screens"
/>
</div>
</template>
@@ -60,7 +62,7 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
color: var(--dark);
}
.global-nav .right-links a:hover{
.global-nav .right-links a:hover {
text-decoration: underline;
}
@@ -15,29 +15,35 @@ limitations under the License.
-->
<script setup>
import Searchbar from '@/features/search/components/Searchbar.vue';
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.name,
(name) => {
searchQueryModel.value =
name === 'searchResults' ? route.query.q || '' : '';
},
);
watch(() => route.query.q, q => {
if (route.name === 'searchResults') {
searchQueryModel.value = q || '';
}
});
watch(
() => route.query.q,
(q) => {
if (route.name === 'searchResults') {
searchQueryModel.value = q || '';
}
},
);
</script>
<template>
<Searchbar
<SearchBar
v-if="route.name === 'searchResults'"
v-model="searchQueryModel"
auto-submit
/>
</template>
</template>
@@ -15,26 +15,25 @@ limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import UIIcon from '@/features/icons/components/UIIcon.vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
const searchQuery = defineModel();
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter()
const props = defineProps(['autoSubmit'])
const router = useRouter();
const props = defineProps(['autoSubmit']);
const submitSearch = function () {
const submitSearch = function submitSearch() {
if (props.autoSubmit !== undefined) {
router.push({
name: 'searchResults',
query: { q: searchQuery.value }
query: { q: searchQuery.value },
});
};
}
}
};
</script>
<template>
@@ -42,10 +41,10 @@ const submitSearch = function () {
<form @submit.prevent="submitSearch" class="search-form">
<div class="search-wrapper">
<input
v-model="searchQuery"
type="search"
:placeholder="t('search.searchBar.placeholder')"
required
v-model="searchQuery"
type="search"
:placeholder="t('search.searchBar.placeholder')"
required
/>
<button
type="submit"
@@ -53,7 +52,11 @@ const submitSearch = function () {
:title="t('search.searchBar.submit')"
:aria-label="t('search.searchBar.submit')"
>
<Icon class="search-icon" name="magnifying-glass" size="1.1em" />
<UIIcon
class="search-icon"
name="magnifying-glass"
size="1.1em"
/>
</button>
</div>
</form>
@@ -70,7 +73,10 @@ const submitSearch = function () {
align-items: center;
border: 1.5px solid var(--border);
box-shadow: 0 0px 32px var(--blue-box-shadow);
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-left: var(--padding-left);
width: 100%;
@@ -91,7 +97,9 @@ const submitSearch = function () {
font-size: 1rem;
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;
padding: var(--submit-button-padding-y) 20px;
background: var(--primary-color);
@@ -114,4 +122,4 @@ const submitSearch = function () {
.search-form {
width: 100%;
}
</style>
</style>
@@ -28,8 +28,7 @@ const { t } = useI18n();
<p>{{ t('error.tryAgainToAnotherTime') }}</p>
</div>
</div>
<div class="search-results-container">
</div>
<div class="search-results-container"></div>
</div>
</template>
@@ -94,4 +93,4 @@ const { t } = useI18n();
transform: scale(1);
}
}
</style>
</style>
@@ -15,33 +15,32 @@ limitations under the License.
-->
<script setup>
import Searchbar from '../features/search/components/Searchbar.vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import logo from '@/assets/images/logo.svg';
import SearchBar from '@/features/search/components/SearchBar.vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter();
const searchQuery = ref('');
const submitSearch = function () {
};
</script>
<template>
<div class="search-content main-content-padding">
<header class="global-header">
<img :src="logo" alt="Seekra" class="header-logo" />
<span class="slogan">
{{ t('slogan') }}
</span>
</header>
<header class="global-header">
<img :src="logo" alt="Seekra" class="header-logo" />
<span class="slogan">
{{ t('slogan') }}
</span>
</header>
<div class="search-container">
<Searchbar v-model="searchQuery" ref="searchbar" class="search-bar" auto-submit />
<SearchBar
v-model="searchQuery"
ref="searchbar"
class="search-bar"
auto-submit
/>
</div>
</div>
</template>
@@ -83,4 +82,4 @@ const submitSearch = function () {
width: 100%;
}
}
</style>
</style>
@@ -16,4 +16,4 @@
.settings-button:hover {
background-color: var(--light-hover);
}
}
@@ -15,10 +15,8 @@ limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import { useSettingsStore } from '../stores/settingsStore';
import UIIcon from '@/features/icons/components/UIIcon.vue';
import { computed, ref, useId, watch } from 'vue';
import { useI18n } from 'vue-i18n';
@@ -26,11 +24,11 @@ const { t } = useI18n();
const props = defineProps({
setting: {
required: true
required: true,
},
path: {
required: true
}
required: true,
},
});
const store = useSettingsStore();
@@ -43,29 +41,29 @@ const doneButtonInitial = ref(true);
const inputId = useId();
const inputLabelId = useId();
const inputType = computed(() => ({
number: 'number',
string: 'text'
}[props.setting.type]
));
const hasChanged = computed(() =>
inputModel.value !== store.get(props.path)
const inputType = computed(
() =>
({
number: 'number',
string: 'text',
})[props.setting.type],
);
const hasChanged = computed(() => inputModel.value !== store.get(props.path));
watch(store, (newValue) => {
const newInputValue = newValue.get(props.path);
if (inputModel.value === oldInputValue) {
inputModel.value = newInputValue;
oldInputValue = newInputValue;
};
}
});
watch(inputModel, () => {
doneButtonInitial.value = false;
});
const apply = function apply () {
const apply = function apply() {
store.set(props.path, inputModel.value);
};
</script>
@@ -95,7 +93,7 @@ const apply = function apply () {
:title="t('settings.input.done.title')"
:aria-label="t('settings.input.done.ariaLabel')"
>
<Icon name="check-mark" />
<UIIcon name="check-mark" />
</button>
</form>
</div>
@@ -131,7 +129,10 @@ const apply = function apply () {
}
.input-container.done-button-shown .input {
width: calc(100% - 1.5 * var(--input-padding) - 26px - 0.75 * var(--input-height) + 1.5px);
width: calc(
100% - 1.5 * var(--input-padding) - 26px - 0.75 * var(--input-height) +
1.5px
);
}
.done-button {
@@ -200,7 +201,7 @@ const apply = function apply () {
opacity: 1;
}
}
@keyframes done-button-disappear {
0% {
opacity: 1;
@@ -212,4 +213,4 @@ const apply = function apply () {
}
}
}
</style>
</style>
@@ -15,22 +15,21 @@ limitations under the License.
-->
<script setup>
import Switch from './Switch.vue';
import SettingsSelection from './SettingsSelection.vue';
import SettingsInput from './SettingsInput.vue';
import Selection from './Selection.vue';
import SettingsPageLink from './SettingsPageLink.vue';
import SettingsSwitch from './SettingsSwitch.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
setting: {
required: true
required: true,
},
path: {
required: true
}
required: true,
},
});
</script>
@@ -44,7 +43,7 @@ const props = defineProps({
:key="setting"
class="setting"
>
<Switch
<SettingsSwitch
v-if="setting.type === 'bool'"
:setting="setting"
:path="`${props.path}.${setting.name}`"
@@ -54,7 +53,7 @@ const props = defineProps({
:setting="setting"
:path="`${props.path}.${setting.name}`"
/>
<Selection
<SettingsSelection
v-if="setting.type === 'selection'"
:setting="setting"
:path="`${props.path}.${setting.name}`"
@@ -79,15 +78,15 @@ const props = defineProps({
margin-bottom: calc(var(--size) - 0.2em);
}
.section-heading[aria-level="2"] {
.section-heading[aria-level='2'] {
--size: 1.8em;
}
.section-heading[aria-level="3"] {
.section-heading[aria-level='3'] {
--size: 1.6em;
}
.section-heading[aria-level="4"] {
.section-heading[aria-level='4'] {
--size: 1.4em;
}
</style>
</style>
@@ -15,8 +15,8 @@ limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import { useSettingsPage } from '../composables/useSettingsPage';
import UIIcon from '@/features/icons/components/UIIcon.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -25,15 +25,15 @@ const { goToSettingsPage } = useSettingsPage();
const props = defineProps({
target: {
required: true,
type: String
type: String,
},
displayName: {
required: true,
type: String
}
type: String,
},
});
const followLink = function followLink () {
const followLink = function followLink() {
goToSettingsPage(props.target);
};
</script>
@@ -42,11 +42,13 @@ const followLink = function followLink () {
<button
class="settings-button button settings-page-link"
:title="props.displayName"
:aria-label="t('settings.pages.link.ariaLabel', { page: props.displayName })"
:aria-label="
t('settings.pages.link.ariaLabel', { page: props.displayName })
"
@click="followLink"
>
{{ props.displayName }}
<Icon name="chevron-right" size="1em" />
<UIIcon name="chevron-right" size="1em" />
</button>
</template>
@@ -58,4 +60,4 @@ const followLink = function followLink () {
align-items: center;
justify-content: space-between;
}
</style>
</style>
@@ -15,14 +15,14 @@ limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import { useSettingsStore } from '../stores/settingsStore';
import { computed, ref, useId, watch } from 'vue';
import UIIcon from '@/features/icons/components/UIIcon.vue';
import { computed, useId } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const normalizeSelectedValue = function normalizeSelectedValue (value) {
const normalizeSelectedValue = function normalizeSelectedValue(value) {
if (props.setting.allowMultiple) {
if (Array.isArray(value)) {
return value;
@@ -44,19 +44,22 @@ const normalizeSelectedValue = function normalizeSelectedValue (value) {
const props = defineProps({
setting: {
required: true
required: true,
},
path: {
required: true
}
required: true,
},
});
const store = useSettingsStore();
const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio');
const optionType = computed(() =>
props.setting.allowMultiple ? 'checkbox' : 'radio',
);
const selected = computed({
get: () => normalizeSelectedValue(store.get(props.path) ?? props.setting.default),
set: (value) => store.set(props.path, value)
get: () =>
normalizeSelectedValue(store.get(props.path) ?? props.setting.default),
set: (value) => store.set(props.path, value),
});
const labelId = useId();
@@ -72,7 +75,11 @@ const labelId = useId();
<label
class="settings-button"
:title="t(option.i18n)"
:aria-label="t('settings.selection.option.ariaLabel', { option: t(option.i18n) })"
:aria-label="
t('settings.selection.option.ariaLabel', {
option: t(option.i18n),
})
"
>
<input
:type="optionType"
@@ -80,9 +87,13 @@ const labelId = useId();
:value="option.name"
class="option-input"
v-model="selected"
>
/>
<div class="option-check-mark-wrapper">
<Icon class="option-check-mark" name="check-mark" size="1em" />
<UIIcon
class="option-check-mark"
name="check-mark"
size="1em"
/>
</div>
<div class="input-label">
{{ t(option.i18n) }}
@@ -122,4 +133,4 @@ const labelId = useId();
.option-input:checked + .option-check-mark-wrapper .option-check-mark {
visibility: visible;
}
</style>
</style>
@@ -23,11 +23,11 @@ const { t } = useI18n();
const props = defineProps({
setting: {
required: true
required: true,
},
path: {
required: true
}
required: true,
},
});
const labelId = useId();
@@ -42,7 +42,7 @@ watch(store, (newStore) => {
// set value after registering watcher to avoid immediate value change
enabled.value = store.get(props.path) ?? props.setting.default;
const toggle = function toggle () {
const toggle = function toggle() {
enabled.value = !enabled.value;
store.set(props.path, enabled.value);
};
@@ -106,4 +106,4 @@ const toggle = function toggle () {
.switch-wrapper.enabled .switch-point {
margin-left: var(--point-size);
}
</style>
</style>
@@ -14,25 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useSettingsStore } from '../../stores/settingsStore.js';
import SettingsInput from '../SettingsInput.vue';
import { mountComponent } from '@/test-utils/mountComponent';
import { useSettingsStore } from '../../stores/settingsStore.js';
import { describe, expect, test } from 'vitest';
import { nextTick } from 'vue';
const getWrapper = function getWrapper({ type = 'string', i18n = 'input', defaultValue = undefined, piniaOptions = {} } = {}) {
return mountComponent(SettingsInput, {
attrs: {
setting: {
name: 'input',
default: defaultValue,
i18n,
type
const getWrapper = function getWrapper({
type = 'string',
i18n = 'input',
defaultValue = undefined,
piniaOptions = {},
} = {}) {
return mountComponent(
SettingsInput,
{
attrs: {
setting: {
name: 'input',
default: defaultValue,
i18n,
type,
},
path: 'input',
},
path: 'input'
}
}, {}, [], piniaOptions);
}
},
{},
[],
piniaOptions,
);
};
describe('SettingsInput', () => {
test('uses correct input type', () => {
@@ -63,8 +74,8 @@ describe('SettingsInput', () => {
setupStores: () => {
const store = useSettingsStore();
store.settings.input = value;
}
}
},
},
});
const inputElement = stringSettingWrapper.find('input');
@@ -76,7 +87,7 @@ describe('SettingsInput', () => {
const wrapper = getWrapper({
type: 'string',
i18n: 'input',
defaultValue: value
defaultValue: value,
});
const inputElement = wrapper.find('input');
@@ -89,13 +100,13 @@ describe('SettingsInput', () => {
const wrapper = getWrapper({
type: 'string',
i18n: 'input',
defaultValue: defaultValue,
defaultValue,
piniaOptions: {
setupStores: () => {
const store = useSettingsStore();
store.settings.input = storeValue;
}
}
},
},
});
const inputElement = wrapper.find('input');
@@ -173,15 +184,15 @@ describe('SettingsInput', () => {
await form.trigger('submit');
expect(form.classes('done-button-shown')).toBeFalsy();
await inputElement.setValue('a');
expect(form.classes('done-button-shown')).toBeTruthy();
const store = useSettingsStore();
store.set('input', 'a');
await nextTick();
expect(form.classes('done-button-shown')).toBeFalsy();
await inputElement.setValue('');
store.set('input', '');
await nextTick();
@@ -191,20 +202,24 @@ describe('SettingsInput', () => {
test('shows correct translation', () => {
const translation = 'Text input';
const wrapper = mountComponent(SettingsInput, {
attrs: {
setting: {
name: 'input',
i18n: 'input',
type: 'string',
const wrapper = mountComponent(
SettingsInput,
{
attrs: {
setting: {
name: 'input',
i18n: 'input',
type: 'string',
},
path: 'input',
},
path: 'input'
}
}, {
input: translation
});
},
{
input: translation,
},
);
const label = wrapper.find('label');
expect(label.text()).toBe(translation);
});
});
});
@@ -14,44 +14,53 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Selection from '../Selection.vue';
import SettingsSelection from '../SettingsSelection.vue';
import SettingsInput from '../SettingsInput.vue';
import SettingsPage from '../SettingsPage.vue';
import SettingsPageLink from '../SettingsPageLink.vue';
import Switch from '../Switch.vue';
import SettingsSwitch from '../SettingsSwitch.vue';
import { mountComponent } from '@/test-utils/mountComponent.js';
import { describe, expect, test } from 'vitest';
const getWrapper = function getWrapper({ content = [], translations = {}, piniaOptions = {} }) {
return mountComponent(SettingsPage, {
attrs: {
setting: {
name: 'section',
type: 'section',
i18n: 'section',
content: content
const getWrapper = function getWrapper({
content = [],
translations = {},
piniaOptions = {},
}) {
return mountComponent(
SettingsPage,
{
attrs: {
setting: {
name: 'section',
type: 'section',
i18n: 'section',
content,
},
path: 'section',
},
path: 'section'
}
}, translations, [], piniaOptions);
},
translations,
[],
piniaOptions,
);
};
const exampleSettings = [
{
type: 'bool',
name: 'b0',
i18n: 'settings.section.b0'
i18n: 'settings.section.b0',
},
{
type: 'number',
name: 'n0',
i18n: 'settings.section.n0'
i18n: 'settings.section.n0',
},
{
type: 'string',
name: 'st0',
i18n: 'settings.section1.st0'
i18n: 'settings.section1.st0',
},
{
type: 'selection',
@@ -61,14 +70,14 @@ const exampleSettings = [
{ name: 'o0', i18n: 'settings.section.se0.o0' },
{ name: 'o1', i18n: 'settings.section.se0.o1' },
{ name: 'o2', i18n: 'settings.section.se0.o2' },
{ name: 'o3', i18n: 'settings.section.se0.o3' }
]
{ name: 'o3', i18n: 'settings.section.se0.o3' },
],
},
{
type: 'section',
name: 's0',
i18n: 'settings.section.s0',
content: []
content: [],
},
{
type: 'section',
@@ -79,17 +88,17 @@ const exampleSettings = [
type: 'bool',
name: 'b0',
i18n: 'settings.section.s1.b0',
default: true
}
]
}
default: true,
},
],
},
];
describe('SettingsPage', () => {
test('shows correct heading', () => {
const translation = 'Example section';
const wrapper = getWrapper({
translations: { section: translation }
translations: { section: translation },
});
const heading = wrapper.find('.section-heading');
@@ -98,20 +107,21 @@ describe('SettingsPage', () => {
test('shows settings correctly', () => {
const wrapper = getWrapper({
content: exampleSettings
content: exampleSettings,
});
const settingsWrappers = wrapper.findAll('.setting');
expect(settingsWrappers.length).toBe(exampleSettings.length);
exampleSettings.forEach((expectedSetting, index) => {
const settingWrapper = settingsWrappers[index];
const switchSetting = settingWrapper.findComponent(Switch);
const switchSetting = settingWrapper.findComponent(SettingsSwitch);
const inputSetting = settingWrapper.findComponent(SettingsInput);
const selectionSetting = settingWrapper.findComponent(Selection);
const settingsPageLink = settingWrapper.findComponent(SettingsPageLink);
const selectionSetting =
settingWrapper.findComponent(SettingsSelection);
const settingsPageLink =
settingWrapper.findComponent(SettingsPageLink);
let setting;
if (expectedSetting.type === 'bool') {
@@ -121,7 +131,10 @@ describe('SettingsPage', () => {
expect(settingsPageLink.exists()).toBe(false);
setting = switchSetting;
} else if (expectedSetting.type === 'string' || expectedSetting.type === 'number') {
} else if (
expectedSetting.type === 'string' ||
expectedSetting.type === 'number'
) {
expect(switchSetting.exists()).toBe(false);
expect(inputSetting.exists()).toBe(true);
expect(selectionSetting.exists()).toBe(false);
@@ -150,11 +163,15 @@ describe('SettingsPage', () => {
expectedSetting.type === 'selection'
) {
expect(setting.props('setting')).toMatchObject(expectedSetting);
expect(setting.props('path')).toBe(`section.${expectedSetting.name}`);
expect(setting.props('path')).toBe(
`section.${expectedSetting.name}`,
);
} else {
expect(setting.props('displayName')).toBe(expectedSetting.i18n);
expect(setting.props('target')).toBe(`section.${expectedSetting.name}`);
expect(setting.props('target')).toBe(
`section.${expectedSetting.name}`,
);
}
});
});
});
});
@@ -22,21 +22,19 @@ import { createI18n } from 'vue-i18n';
const goToSettingsPage = vi.fn();
vi.mock('../../composables/useSettingsPage.js', () => ({
useSettingsPage: () => ({
goToSettingsPage
})
goToSettingsPage,
}),
}));
const getWrapper = function getWrapper({ target, displayName }) {
return mount(SettingsPageLink, {
attrs: {
target,
displayName
displayName,
},
global: {
plugins: [
createI18n()
]
}
plugins: [createI18n()],
},
});
};
@@ -56,4 +54,4 @@ describe('SettingsPageLink', () => {
expect(goToSettingsPage).toHaveBeenCalledWith(target);
});
});
});
@@ -14,46 +14,52 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Selection from '../Selection.vue';
import { mountComponent } from '@/test-utils/mountComponent.js';
import { useSettingsStore } from '../../stores/settingsStore.js';
import SettingsSelection from '../SettingsSelection.vue';
import { mountComponent } from '@/test-utils/mountComponent.js';
import { expect, describe, test } from 'vitest';
import { nextTick } from 'vue';
const getWrapper = function getWrapper ({
const getWrapper = function getWrapper({
i18n = 'selection',
options = [],
allowMultiple = false,
defaultValue = undefined,
translations = {},
piniaOptions = {}
piniaOptions = {},
} = {}) {
return mountComponent(Selection, {
attrs: {
setting: {
name: 'selection',
type: 'selection',
i18n,
options,
allowMultiple,
default: defaultValue
return mountComponent(
SettingsSelection,
{
attrs: {
setting: {
name: 'selection',
type: 'selection',
i18n,
options,
allowMultiple,
default: defaultValue,
},
path: 'selection',
},
path: 'selection'
}
}, translations, [], piniaOptions);
},
translations,
[],
piniaOptions,
);
};
const getChecked = function getChecked (inputs) {
const getChecked = function getChecked(inputs) {
return inputs.map((input) => input.element.matches(':checked'));
};
const exampleOptions = [
{ name: 'o0', i18n: 'o0' },
{ name: 'o1', i18n: 'o1' },
{ name: 'o2', i18n: 'o2' }
{ name: 'o2', i18n: 'o2' },
];
describe('Selection', () => {
describe('SettingsSelection', () => {
test('displays options correctly', () => {
const wrapper = getWrapper({ options: exampleOptions });
@@ -71,7 +77,7 @@ describe('Selection', () => {
expect(input.attributes('name')).toBe('selection');
expect(input.element.matches(':checked')).toBe(false);
};
}
});
test('shows value from store', () => {
@@ -81,8 +87,8 @@ describe('Selection', () => {
setupStores: () => {
const store = useSettingsStore();
store.set('selection', 'o1');
}
}
},
},
});
const inputs = wrapper.findAll('input');
@@ -97,8 +103,8 @@ describe('Selection', () => {
setupStores: () => {
const store = useSettingsStore();
store.set('selection', 'o1');
}
}
},
},
});
const settings = useSettingsStore();
@@ -123,9 +129,9 @@ describe('Selection', () => {
setupStores: () => {
const store = useSettingsStore();
store.set('selection', ['o1', 'o2']);
}
},
},
allowMultiple: true
allowMultiple: true,
});
const inputs = wrapper.findAll('input');
@@ -136,7 +142,7 @@ describe('Selection', () => {
test('shows default value', () => {
const wrapper = getWrapper({
options: exampleOptions,
defaultValue: 'o2'
defaultValue: 'o2',
});
const inputs = wrapper.findAll('input');
@@ -148,7 +154,7 @@ describe('Selection', () => {
const wrapper = getWrapper({
options: exampleOptions,
defaultValue: ['o0', 'o1'],
allowMultiple: true
allowMultiple: true,
});
const inputs = wrapper.findAll('input');
@@ -164,8 +170,8 @@ describe('Selection', () => {
setupStores: () => {
const store = useSettingsStore();
store.set('selection', 'o1');
}
}
},
},
});
const inputs = wrapper.findAll('input');
@@ -199,33 +205,36 @@ describe('Selection', () => {
});
test('stores multiple values', async () => {
const setValue = async function setValue (input, value) {
const setValue = async function setValue(input, value) {
input.setValue(value);
await nextTick();
};
const check = function check (input) {
const check = function check(input) {
return setValue(input, true);
};
const uncheck = function uncheck (input) {
const uncheck = function uncheck(input) {
return setValue(input, false);
};
const expectSelectionValue = function expectSelectionValue (value, options = exampleOptions) {
const expectSelectionValue = function expectSelectionValue(
value,
options = exampleOptions,
) {
value.sort();
expect(store.get('selection').sort()).toStrictEqual(value);
const checkedInputs = getChecked(inputs);
expect(
options
.map((option) => option.name)
.filter((entry, i) => checkedInputs[i])
.map((option) => option.name)
.filter((_, i) => checkedInputs[i]),
).toStrictEqual(value);
};
const wrapper = getWrapper({
options: exampleOptions,
allowMultiple: true
allowMultiple: true,
});
const store = useSettingsStore();
@@ -237,7 +246,7 @@ describe('Selection', () => {
const inputO2 = inputs[2];
await check(inputO0);
expectSelectionValue(['o0'])
expectSelectionValue(['o0']);
await uncheck(inputO0);
expectSelectionValue([]);
@@ -283,11 +292,11 @@ describe('Selection', () => {
const translation = 'Selection setting';
const wrapper = getWrapper({
translations: {
selection: translation
}
selection: translation,
},
});
const label = wrapper.find('.label');
expect(label.text()).toBe(translation);
});
});
});
@@ -14,32 +14,38 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Switch from '../Switch.vue';
import { mountComponent } from '@/test-utils/mountComponent.js';
import { useSettingsStore } from '../../stores/settingsStore.js';
import { expect, describe, test } from 'vitest';
import SettingsSwitch from '../SettingsSwitch.vue';
import { mountComponent } from '@/test-utils/mountComponent.js';
import { describe, expect, test } from 'vitest';
import { nextTick } from 'vue';
const getWrapper = function getWrapper ({
const getWrapper = function getWrapper({
i18n = 'switch1',
defaultValue = undefined,
translations = {},
piniaOptions = {}
piniaOptions = {},
} = {}) {
return mountComponent(Switch, {
attrs: {
setting: {
type: 'bool',
name: 'switch',
i18n,
default: defaultValue
return mountComponent(
SettingsSwitch,
{
attrs: {
setting: {
type: 'bool',
name: 'switch',
i18n,
default: defaultValue,
},
path: 'switch',
},
path: 'switch'
}
}, translations, [], piniaOptions);
},
translations,
[],
piniaOptions,
);
};
describe('Switch', () => {
describe('SettingsSwitch', () => {
test('shows value from store', async () => {
const wrapper = getWrapper();
@@ -61,7 +67,7 @@ describe('Switch', () => {
test('shows default value', () => {
const wrapper = getWrapper({
defaultValue: true
defaultValue: true,
});
const switchElement = wrapper.find('.switch-wrapper');
@@ -75,8 +81,8 @@ describe('Switch', () => {
setupStores: () => {
const store = useSettingsStore();
store.set('switch', false);
}
}
},
},
});
const switchElement = wrapper.find('.switch-wrapper');
@@ -89,7 +95,7 @@ describe('Switch', () => {
const switchElement = wrapper.find('.switch-wrapper');
expect(switchElement.classes('enabled')).toBeFalsy();
store.set('switch', true);
await nextTick();
expect(switchElement.classes('enabled')).toBeTruthy();
@@ -97,29 +103,29 @@ describe('Switch', () => {
test('toggles value in store', async () => {
const wrapper = getWrapper();
const switchElement = wrapper.find('.switch-wrapper');
const store = useSettingsStore();
expect(store.get('switch')).toBeUndefined();
switchElement.trigger('click');
await nextTick();
expect(store.get('switch')).toBe(true);
switchElement.trigger('click');
await nextTick();
expect(store.get('switch')).toBe(false);
});
test('shows correct translation', () => {
const translation = 'Switch';
const wrapper = getWrapper({
translations: { switch1: translation }
})
translations: { switch1: translation },
});
const label = wrapper.find('label');
expect(label.text()).toBe(translation);
});
});
});
@@ -33,17 +33,17 @@ vi.mock('../../config.js', () => ({
type: 'number',
i18n: '',
name: 'number',
default: settingDefaultValue
default: settingDefaultValue,
},
{
type: 'bool',
i18n: '',
name: 'bool'
}
]
}
]
}
name: 'bool',
},
],
},
],
},
}));
describe('useSettings', () => {
@@ -59,7 +59,7 @@ describe('useSettings', () => {
});
test('returns default value', () => {
const { getSetting, setSetting } = useSettings();
const { getSetting } = useSettings();
expect(getSetting(settingPath)).toBe(settingDefaultValue);
});
@@ -85,7 +85,7 @@ beforeEach(() => {
setActivePinia(
createTestingPinia({
createSpy: vi.fn,
stubActions: false
})
stubActions: false,
}),
);
});
@@ -20,8 +20,8 @@ import { describe, expect, test, vi } from 'vitest';
const pushRoute = vi.fn();
vi.mock('vue-router', () => ({
useRouter: () => ({
push: pushRoute
})
push: pushRoute,
}),
}));
describe('useSettingsPage', () => {
@@ -32,9 +32,9 @@ describe('useSettingsPage', () => {
expect(pushRoute).toHaveBeenCalledWith({
name: 'settings',
params: {
rest: ['a', 'b', 'c', 'd', 'e']
}
rest: ['a', 'b', 'c', 'd', 'e'],
},
});
});
});
});
});
@@ -21,20 +21,18 @@ import { getSettingRecursively } from '../utils/getSetting';
/**
* Provides access to the stored settings.
*/
export const useSettings = function useSettings () {
export const useSettings = function useSettings() {
/**
* Returns the value of a specific setting.
* @param {string[]} key - The settings key.
* @return The value of the setting.
*/
const getSetting = function getSetting (key) {
const getSetting = function getSetting(key) {
const settingsStore = useSettingsStore();
return (
settingsStore.get(key.join('.')) ??
getSettingRecursively(
key, settings.contents ?? []
)?.default
getSettingRecursively(key, settings.contents ?? [])?.default
);
};
@@ -43,7 +41,7 @@ export const useSettings = function useSettings () {
* @param {string[]} key - The setting key.
* @param value - The new value for the setting.
*/
const setSetting = function setSetting (key, value) {
const setSetting = function setSetting(key, value) {
const settingsStore = useSettingsStore();
settingsStore.set(key.join('.'), value);
};
@@ -17,17 +17,17 @@ limitations under the License.
import { getSettingsPagePathSegments } from '../utils/settingsPage';
import { useRouter } from 'vue-router';
export const useSettingsPage = function useSettingsPage () {
export const useSettingsPage = function useSettingsPage() {
const router = useRouter();
const goToSettingsPage = function goToSettingsPage (pagePath) {
const goToSettingsPage = function goToSettingsPage(pagePath) {
return router.push({
name: 'settings',
params: {
rest: getSettingsPagePathSegments(pagePath)
}
rest: getSettingsPagePathSegments(pagePath),
},
});
};
return { goToSettingsPage };
};
};
+13 -13
View File
@@ -40,10 +40,10 @@ export default {
default: defaultLanguage,
options: SUPPORTED_LANGUAGES.map((language) => ({
name: language,
i18n: `preferences.locale.languages.${language}`
}))
}
]
i18n: `preferences.locale.languages.${language}`,
})),
},
],
},
{
name: 'appearance',
@@ -57,19 +57,19 @@ export default {
options: [
{
name: 'auto',
i18n: 'settings.settings.appearance.contents.colorScheme.options.auto'
i18n: 'settings.settings.appearance.contents.colorScheme.options.auto',
},
{
name: 'light',
i18n: 'settings.settings.appearance.contents.colorScheme.options.light'
i18n: 'settings.settings.appearance.contents.colorScheme.options.light',
},
{
name: 'dark',
i18n: 'settings.settings.appearance.contents.colorScheme.options.dark'
}
]
}
]
}
]
i18n: 'settings.settings.appearance.contents.colorScheme.options.dark',
},
],
},
],
},
],
};
+22 -18
View File
@@ -17,26 +17,30 @@ limitations under the License.
import { defineStore } from 'pinia';
import { reactive } from 'vue';
export const useSettingsStore = defineStore('settings', () => {
const settings = reactive({});
export const useSettingsStore = defineStore(
'settings',
() => {
const settings = reactive({});
const set = function set (key, value) {
settings[key] = value;
};
const set = function set(key, value) {
settings[key] = value;
};
const get = function get (key) {
return settings[key];
};
const get = function get(key) {
return settings[key];
};
const remove = function remove (key) {
delete settings[key];
};
const remove = function remove(key) {
delete settings[key];
};
const clear = function clear () {
Object.keys(settings).forEach(key => delete settings[key]);
};
const clear = function clear() {
Object.keys(settings).forEach((key) => delete settings[key]);
};
return { set, get, remove, clear, settings };
}, {
persist: true
});
return { set, get, remove, clear, settings };
},
{
persist: true,
},
);
@@ -62,4 +62,4 @@ limitations under the License.
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
* @typedef {{ contents: SettingConfigEntry[] }} FirstLevelSettingConfigEntry
* @typedef {{ contents: FirstLevelSettingConfigEntry[] }} SettingsConfig
*/
*/
File diff suppressed because it is too large Load Diff
@@ -47,17 +47,26 @@ describe('settingsPage', () => {
{ path: 'a1.#b.c/', expected: ['a1', 'b', 'c'] },
{ path: 'a-1.#b.c/', expected: ['a-1', 'b', 'c'] },
{ path: 'a-1.b@.c', expected: ['a-1', 'b', 'c'] },
{ path: '....@a/#...)!&§[b.#§c..d....', expected: ['a', 'b', 'c', 'd'] },
{ path: '....@a/#...)!&§[b.#§c..dä....', expected: ['a', 'b', 'c', 'd'] },
{ path: '..,...~..@a/#.+..)!&§[b.#§c..dä..)..', expected: ['a', 'b', 'c', 'd'] },
{
path: '....@a/#...)!&§[b.#§c..d....',
expected: ['a', 'b', 'c', 'd'],
},
{
path: '....@a/#...)!&§[b.#§c..dä....',
expected: ['a', 'b', 'c', 'd'],
},
{
path: '..,...~..@a/#.+..)!&§[b.#§c..dä..)..',
expected: ['a', 'b', 'c', 'd'],
},
{ path: 'a.@.b', expected: ['a', 'b'] },
{ path: 1, expected: [] },
{ path: false, expected: [] },
{ path: true, expected: [] },
{ path: null, expected: [] },
{ path: undefined, expected: [] }
])('returns $expected', async ({ path, expected }) => {
{ path: undefined, expected: [] },
])('returns $expected', ({ path, expected }) => {
expect(getSettingsPagePathSegments(path)).toStrictEqual(expected);
});
})
})
});
});
File diff suppressed because it is too large Load Diff
+13 -7
View File
@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export const getSetting = function getSetting (name, settings) {
export const getSetting = function getSetting(name, settings) {
return settings.filter((setting) => setting.name === name)[0];
};
export const getSettingRecursively = function getSettingRecursively (sectionPath, settings) {
const oneSettingsLevel = function oneSettingsLevel (cursor, acc) {
export const getSettingRecursively = function getSettingRecursively(
sectionPath,
settings,
) {
const oneSettingsLevel = function oneSettingsLevel(cursor, acc) {
const setting = getSetting(cursor[0], acc.content);
if (!setting) return;
@@ -28,15 +31,18 @@ export const getSettingRecursively = function getSettingRecursively (sectionPath
const newCursor = cursor.slice(1, cursor.length);
if (newCursor.length !== 0) {
return oneSettingsLevel(cursor.slice(1, cursor.length), setting);
return oneSettingsLevel(
cursor.slice(1, cursor.length),
setting,
);
} else {
return setting;
};
}
} else if (cursor.length <= 1) {
return setting;
} else {
return;
};
}
};
return oneSettingsLevel(sectionPath, { content: settings });
};
};
+5 -5
View File
@@ -19,13 +19,13 @@ limitations under the License.
* @param {string} path - The settings page path.
* @returns {Array} The settings page path segments.
*/
export const getSettingsPagePathSegments = function getSettingsPagePathSegments (path) {
export const getSettingsPagePathSegments = function getSettingsPagePathSegments(
path,
) {
if (typeof path !== 'string') return [];
return path
.replace(/^\.+|\.+$/g, '')
.split('.')
.map(
(segment) => segment.replace(/[^a-zA-Z0-9-]/g, '')
)
.map((segment) => segment.replace(/[^a-zA-Z0-9-]/g, ''))
.filter(Boolean);
};
};
@@ -16,33 +16,38 @@ limitations under the License.
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
export const assertString = function assertString (value, path) {
export const assertString = function assertString(value, path) {
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`[settings] "${path}" must be a non-empty string`);
}
}
};
export const assertType = function assertType (value, path) {
export const assertType = function assertType(value, path) {
if (!VALID_TYPES.includes(value)) {
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(', ')}`,
);
}
}
};
export const validateSelectionOptions = function validateSelectionOptions (options, path) {
export const validateSelectionOptions = function validateSelectionOptions(
options,
path,
) {
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) => {
assertString(opt.name, `${path}.options[${i}].name`);
assertString(opt.i18n, `${path}.options[${i}].i18n`);
});
}
};
export const validateEntry = function validateEntry (entry, path) {
export const validateEntry = function validateEntry(entry, path) {
assertType(entry.type, `${path}.type`);
assertString(entry.name, `${path}.name`);
assertString(entry.i18n, `${path}.i18n`);
@@ -51,17 +56,19 @@ export const validateEntry = function validateEntry (entry, path) {
throw new Error(`[settings] "${path}.content" must be an array`);
}
entry.content.forEach((child, i) =>
validateEntry(child, `${path}.content[${i}]`)
validateEntry(child, `${path}.content[${i}]`),
);
return;
}
if (entry.type === 'selection') {
validateSelectionOptions(entry.options, path);
if (typeof entry.allowMultiple !== 'boolean' && entry.allowMultiple) {
throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`);
throw new Error(
`[settings] "${path}.allowMultiple" must be a boolean`,
);
}
};
}
if (entry.default !== undefined) {
if (entry.type === 'bool' && typeof entry.default !== 'boolean') {
throw new Error(`[settings] "${path}.default" must be a boolean`);
@@ -75,39 +82,56 @@ export const validateEntry = function validateEntry (entry, path) {
if (entry.type === 'selection') {
if (entry.allowMultiple) {
if (!Array.isArray(entry.default)) {
throw new Error(`[settings] "${path}.default" must be an array`);
throw new Error(
`[settings] "${path}.default" must be an array`,
);
}
const allOptions = entry.options.map((option) => option.name);
entry.default.forEach((defaultValue, index) => {
if (typeof defaultValue !== 'string') {
throw new Error(`[settings] "${path}.default[${index}]" must be a string`);
throw new Error(
`[settings] "${path}.default[${index}]" must be a string`,
);
}
if (!allOptions.includes(defaultValue)) {
throw new Error(`[settings] option "${path}.default[${index}]" does not exist`);
throw new Error(
`[settings] option "${path}.default[${index}]" does not exist`,
);
}
});
} else {
if (typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`);
};
if (!entry.options.map((option) => option.name).includes(entry.default)) {
throw new Error(`[settings] option "${path}.default" does not exist`);
};
throw new Error(
`[settings] "${path}.default" must be a string`,
);
}
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) {
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}]`)
validateEntry(entry, `${path}.content[${i}]`),
);
};
@@ -116,7 +140,7 @@ export const validateFirstLevelSection = function validateFirstLevelSection (sec
* @param {unknown} raw
* @returns {{ valid: true, config: import('../types/settingsConfig').SettingsConfig } | { valid: false, error: string }}
*/
export function validateSettingsConfig(raw) {
export const validateSettingsConfig = function validateSettingsConfig(raw) {
if (!raw || typeof raw !== 'object') {
throw new Error('[settings] Config must be an object');
}
@@ -124,6 +148,6 @@ export function validateSettingsConfig(raw) {
throw new Error('[settings] "contents" must be an array');
}
raw.contents.forEach((entry, i) =>
validateFirstLevelSection(entry, `contents[${i}]`)
validateFirstLevelSection(entry, `contents[${i}]`),
);
}
};
+13 -10
View File
@@ -15,11 +15,10 @@ limitations under the License.
-->
<script setup>
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
import SettingsPage from '../components/SettingsPage.vue';
import settingsConfiguration from '../config.js';
import { getSettingRecursively } from '../utils/getSetting.js';
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
@@ -31,15 +30,13 @@ const router = useRouter();
const settings = settingsConfiguration.contents;
const activePath = computed(() => {
const segments = route.path
.split('/')
.filter(Boolean);
const segments = route.path.split('/').filter(Boolean);
return segments.slice(1, segments.length);
});
const activeSection = ref(null);
const updateSettings = async function updateSettings () {
const updateSettings = async function updateSettings() {
const setting = getSettingRecursively(activePath.value, settings);
if (setting) {
activeSection.value = setting;
@@ -50,7 +47,9 @@ const updateSettings = async function updateSettings () {
watch(route, (newRoute) => updateSettings(newRoute));
onMounted(updateSettings);
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSidebarFunction) {
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen(
toggleSidebarFunction,
) {
if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction();
};
</script>
@@ -65,11 +64,15 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi
<LeftSidebarLayout class="layout">
<template #sidebar="{ toggleSidebar }">
<ul class="sidebar-sections-list">
<li v-for="section in settings">
<li v-for="section in settings" :key="section">
<RouterLink
:to="`/settings/${section.name}`"
class="button button-link link sidebar-section"
:class="{ active: activeSection && activeSection.name === section.name }"
:class="{
active:
activeSection &&
activeSection.name === section.name,
}"
@click="toggleSidebarIfSmallScreen(toggleSidebar)"
>
{{ t(section.i18n) }}
@@ -137,4 +140,4 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi
padding: 0;
}
}
</style>
</style>
@@ -23,7 +23,7 @@ const settingsHoisted = vi.hoisted(() => [
{
name: 's0',
i18n: 'settings.s0',
content: []
content: [],
},
{
name: 's1',
@@ -32,17 +32,17 @@ const settingsHoisted = vi.hoisted(() => [
{
type: 'bool',
name: 'b0',
i18n: 'settings.s1.b0'
i18n: 'settings.s1.b0',
},
{
type: 'number',
name: 'n0',
i18n: 'settings.s1.n0'
i18n: 'settings.s1.n0',
},
{
type: 'string',
name: 'st0',
i18n: 'settings.s1.st0'
i18n: 'settings.s1.st0',
},
{
type: 'selection',
@@ -52,14 +52,14 @@ const settingsHoisted = vi.hoisted(() => [
{ name: 'o0', i18n: 'settings.s1.se0.o0' },
{ name: 'o1', i18n: 'settings.s1.se0.o1' },
{ name: 'o2', i18n: 'settings.s1.se0.o2' },
{ name: 'o3', i18n: 'settings.s1.se0.o3' }
]
{ name: 'o3', i18n: 'settings.s1.se0.o3' },
],
},
{
type: 'section',
name: 's0',
i18n: 'settings.s1.s0',
content: []
content: [],
},
{
type: 'section',
@@ -70,11 +70,11 @@ const settingsHoisted = vi.hoisted(() => [
type: 'bool',
name: 'b0',
i18n: 'settings.s1.s1.b0',
default: true
}
]
}
]
default: true,
},
],
},
],
},
{
name: 's2',
@@ -89,11 +89,11 @@ const settingsHoisted = vi.hoisted(() => [
type: 'bool',
name: 'b0',
i18n: 'settings.s2.s0.b0',
default: true
}
]
}
]
default: true,
},
],
},
],
},
{
name: 's3',
@@ -103,9 +103,9 @@ const settingsHoisted = vi.hoisted(() => [
type: 'section',
name: 's0',
i18n: 'settings.s3.s0',
content: []
}
]
content: [],
},
],
},
{
name: 's4',
@@ -119,10 +119,10 @@ const settingsHoisted = vi.hoisted(() => [
{ name: 'o0', i18n: 'settings.s4.se0.o0' },
{ name: 'o1', i18n: 'settings.s4.se0.o1' },
{ name: 'o2', i18n: 'settings.s4.se0.o2' },
{ name: 'o3', i18n: 'settings.s4.se0.o3' }
]
}
]
{ name: 'o3', i18n: 'settings.s4.se0.o3' },
],
},
],
},
{
name: 's5',
@@ -131,9 +131,9 @@ const settingsHoisted = vi.hoisted(() => [
{
type: 'string',
name: 'st0',
i18n: 'settings.s5.st0'
}
]
i18n: 'settings.s5.st0',
},
],
},
{
name: 's6',
@@ -142,9 +142,9 @@ const settingsHoisted = vi.hoisted(() => [
{
type: 'number',
name: 'n0',
i18n: 'settings.s6.n0'
}
]
i18n: 'settings.s6.n0',
},
],
},
{
name: 's7',
@@ -153,9 +153,9 @@ const settingsHoisted = vi.hoisted(() => [
{
type: 'bool',
name: 'b0',
i18n: 'settings.s7.b0'
}
]
i18n: 'settings.s7.b0',
},
],
},
{
name: 's8',
@@ -181,16 +181,16 @@ const settingsHoisted = vi.hoisted(() => [
name: 'se0',
i18n: 'settings.s8.s0.s0.s0.se0',
allowMultiple: true,
options: []
}
]
}
]
}
]
}
]
}
options: [],
},
],
},
],
},
],
},
],
},
]);
const settings = settingsHoisted.values();
@@ -198,7 +198,7 @@ const originalCurrentSection = 's0';
let currentSection = originalCurrentSection;
vi.mock('../../config.js', () => ({
default: { contents: settingsHoisted }
default: { contents: settingsHoisted },
}));
vi.mock('vue-router', async (importOriginal) => {
@@ -206,12 +206,12 @@ vi.mock('vue-router', async (importOriginal) => {
return {
...actual,
useRoute: () => ({
path: `/settings/${currentSection}`
})
path: `/settings/${currentSection}`,
}),
};
});
const getWrapper = function getWrapper ({ translations = {} } = {}) {
const getWrapper = function getWrapper({ translations = {} } = {}) {
return mountComponent(SettingsView, {}, translations, [], {});
};
@@ -221,8 +221,8 @@ describe('SettingsView', () => {
const translation = 'Settings';
const wrapper = getWrapper({
translations: {
'preferences.settings': translation
}
'preferences.settings': translation,
},
});
const header = wrapper.find('header');
@@ -242,8 +242,12 @@ describe('SettingsView', () => {
expect(sidebarItem.text()).toBe(setting.i18n);
const link = sidebarItem.find('*');
expect(link.attributes('href')).toBe(`/settings/${setting.name}`);
expect(link.classes('active')).toBe(setting.name === currentSection);
expect(link.attributes('href')).toBe(
`/settings/${setting.name}`,
);
expect(link.classes('active')).toBe(
setting.name === currentSection,
);
});
});
});
@@ -257,7 +261,9 @@ describe('SettingsView', () => {
await nextTick();
const mainContent = wrapper.find('.settings-main-content');
const mainContentNoSection = wrapper.find('.main-content--no-section');
const mainContentNoSection = wrapper.find(
'.main-content--no-section',
);
expect(mainContent.exists()).toBe(false);
expect(mainContentNoSection.exists()).toBe(true);
@@ -268,7 +274,9 @@ describe('SettingsView', () => {
await nextTick();
const mainContent = wrapper.find('.settings-main-content');
const mainContentNoSection = wrapper.find('.main-content--no-section');
const mainContentNoSection = wrapper.find(
'.main-content--no-section',
);
expect(mainContent.exists()).toBe(true);
expect(mainContentNoSection.exists()).toBe(false);
@@ -279,4 +287,4 @@ describe('SettingsView', () => {
beforeEach(() => {
currentSection = originalCurrentSection;
});
});
@@ -18,15 +18,15 @@ limitations under the License.
import SidebarExpandButton from './SidebarExpandButton.vue';
const props = defineProps({
'expanded': {
expanded: {
default: false,
required: false,
type: Boolean
}
type: Boolean,
},
});
const emit = defineEmits(['toggleExpanded']);
const toggleSidebar = function toggleSidebar () {
const toggleSidebar = function toggleSidebar() {
emit('toggleExpanded');
};
</script>
@@ -34,7 +34,10 @@ const toggleSidebar = function toggleSidebar () {
<template>
<nav class="sidebar" :class="{ expanded }">
<div class="sidebar-controls">
<SidebarExpandButton @click="toggleSidebar" class="sidebar-expand-button" />
<SidebarExpandButton
@click="toggleSidebar"
class="sidebar-expand-button"
/>
</div>
<div class="sidebar-content" v-show="props.expanded">
<slot />
@@ -74,4 +77,4 @@ const toggleSidebar = function toggleSidebar () {
padding: 20px;
padding-top: 0;
}
</style>
</style>
@@ -15,18 +15,18 @@ limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import UIIcon from '@/features/icons/components/UIIcon.vue';
const emit = defineEmits(['click']);
const click = function click (event) {
const click = function click(event) {
emit('click', event);
};
</script>
<template>
<button class="button sidebar-expand-button">
<Icon name="sidebar-expand" @click="click" />
<UIIcon name="sidebar-expand" @click="click" />
</button>
</template>
@@ -38,4 +38,4 @@ const click = function click (event) {
padding: 4px;
border-radius: 10px;
}
</style>
</style>
+10 -18
View File
@@ -18,29 +18,19 @@ import { createI18n } from 'vue-i18n';
export const fallbackLocale = 'en';
export const LANGUAGES_RTL = [
'ar'
];
export const SUPPORTED_LANGUAGES = [
'en',
'de',
'fr',
'es',
'pt',
'it'
];
export const LANGUAGES_RTL = ['ar'];
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt', 'it'];
export const i18n = createI18n({
legacy: false,
locale: fallbackLocale,
fallbackLocale: fallbackLocale,
messages: {}
fallbackLocale,
messages: {},
});
const loadedLanguages = new Set();
export async function loadLanguage (locale) {
export const loadLanguage = async function loadLanguage(locale) {
if (!SUPPORTED_LANGUAGES.includes(locale)) {
locale = fallbackLocale;
}
@@ -48,7 +38,7 @@ export async function loadLanguage (locale) {
if (loadedLanguages.has(locale)) {
i18n.global.locale.value = locale;
return;
};
}
const messages = (await import(`./locales/${locale}.json`)).default;
@@ -58,8 +48,10 @@ export async function loadLanguage (locale) {
loadedLanguages.add(locale);
};
export const setLocale = async function setLocale (locale) {
export const setLocale = async function setLocale(locale) {
await loadLanguage(locale);
document.documentElement.lang = locale;
document.documentElement.dir = LANGUAGES_RTL.includes(locale) ? 'rtl' : 'ltr';
document.documentElement.dir = LANGUAGES_RTL.includes(locale)
? 'rtl'
: 'ltr';
};
+19 -12
View File
@@ -15,21 +15,21 @@ limitations under the License.
-->
<script setup>
import Sidebar from '@/features/sidebar/components/Sidebar.vue';
import AppSidebar from '@/features/sidebar/components/AppSidebar.vue';
import SidebarExpandButton from '@/features/sidebar/components/SidebarExpandButton.vue';
import { ref } from 'vue';
const props = defineProps({
'expanded': {
expanded: {
default: false,
required: false,
type: Boolean
}
type: Boolean,
},
});
const expanded = ref(props.expanded);
const toggleSidebar = function toggleSidebar () {
const toggleSidebar = function toggleSidebar() {
expanded.value = !expanded.value;
};
</script>
@@ -37,11 +37,18 @@ const toggleSidebar = function toggleSidebar () {
<template>
<div class="layout-container" :class="{ expanded }">
<div class="sidebar-expand-button-container">
<SidebarExpandButton class="sidebar-expand-button" @click="toggleSidebar" />
<SidebarExpandButton
class="sidebar-expand-button"
@click="toggleSidebar"
/>
</div>
<Sidebar @toggle-expanded="toggleSidebar" :expanded="expanded" class="sidebar">
<AppSidebar
@toggle-expanded="toggleSidebar"
:expanded="expanded"
class="sidebar"
>
<slot name="sidebar" :toggleSidebar="toggleSidebar" />
</Sidebar>
</AppSidebar>
<main class="main-content">
<slot :toggleSidebar="toggleSidebar" />
</main>
@@ -54,8 +61,8 @@ const toggleSidebar = function toggleSidebar () {
grid-template-columns: calc(32px + 2em) 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"sidebar-expand sidebar-expand"
"sidebar main-content";
'sidebar-expand sidebar-expand'
'sidebar main-content';
border-top: 1px solid var(--border);
}
@@ -80,7 +87,7 @@ const toggleSidebar = function toggleSidebar () {
.layout-container.expanded {
grid-template-columns: 1fr 0;
}
.layout-container:not(.expanded) .sidebar-expand-button-container {
justify-content: end;
display: block;
@@ -100,4 +107,4 @@ const toggleSidebar = function toggleSidebar () {
grid-area: main-content;
padding: 20px;
}
</style>
</style>
+1 -1
View File
@@ -91,4 +91,4 @@
"loadError": "Das Impressum konnte nicht geladen werden."
}
}
}
}
+1 -1
View File
@@ -91,4 +91,4 @@
"loadError": "The legal notice could not be loaded."
}
}
}
}
+1 -1
View File
@@ -62,4 +62,4 @@
}
}
}
}
}
+1 -1
View File
@@ -62,4 +62,4 @@
}
}
}
}
}
+2 -2
View File
@@ -42,7 +42,7 @@
"settings": {
"input": {
"done": {
"title": "Salva valore",
"title": "Salva valore",
"ariaLabel": "Salva il valore inserito"
}
},
@@ -62,4 +62,4 @@
}
}
}
}
}
+2 -2
View File
@@ -42,7 +42,7 @@
"settings": {
"input": {
"done": {
"title": "Guardar valor",
"title": "Guardar valor",
"ariaLabel": "Guarda o valor introduzido"
}
},
@@ -62,4 +62,4 @@
}
}
}
}
}
+36 -32
View File
@@ -14,40 +14,48 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { getAssetsUrl } from './config/env';
import { createApp } from 'vue'
import { i18n, loadLanguage, fallbackLocale } from './i18n';
import router from './router';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue'
import { i18n, loadLanguage, fallbackLocale } from './i18n';
import router from './router'
import './styles/common.css'
import './styles/variables/colors.css'
import { createApp } from 'vue';
import App from './App.vue';
import './styles/common.css';
import rawFonts from './styles/fonts.json';
import './styles/variables/colors.css';
import './styles/variables/dimensions.css';
import rawFonts from './styles/fonts.json';
import './styles/common.css';
import './styles/variables/colors.css';
import settings from './features/settings/config.js';
import { getAssetsUrl } from './config/env';
import { validateSettingsConfig } from './features/settings/utils/settingsValidator';
(async () => {
const fonts = rawFonts.map((font) => new FontFace(
font.family ?? '',
Array.isArray(font.source)
? font.source.map(
(source) => `url('${source.url
? new URL(source.url, getAssetsUrl()).href
: ''
}') format('${source.format ?? ''}')`
).join(',')
: '',
{
weight: font.weight ?? undefined,
style: font.style ?? undefined
}
));
const fonts = rawFonts.map(
(font) =>
new FontFace(
font.family ?? '',
Array.isArray(font.source)
? font.source
.map(
(source) =>
`url('${
source.url
? new URL(source.url, getAssetsUrl())
.href
: ''
}') format('${source.format ?? ''}')`,
)
.join(',')
: '',
{
weight: font.weight ?? undefined,
style: font.style ?? undefined,
},
),
);
await Promise.all(fonts.map((font) => font.load()));
fonts.forEach((font) => document.fonts.add(font));
@@ -57,10 +65,6 @@ import { validateSettingsConfig } from './features/settings/utils/settingsValida
pinia.use(piniaPluginPersistedstate);
validateSettingsConfig(settings);
createApp(App)
.use(router)
.use(i18n)
.use(pinia)
.mount('#app');
createApp(App).use(router).use(i18n).use(pinia).mount('#app');
})();
+23 -23
View File
@@ -14,71 +14,71 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { createRouter, createWebHistory } from 'vue-router';
import { i18n } from '@/i18n';
import SearchView from '../views/SearchView.vue';
import LegalNoticeView from '@/features/legal/views/LegalNoticeView.vue';
import SearchResultsView from '@/features/search/views/SearchResultsView.vue';
import SearchView from '@/features/search/views/SearchView.vue';
import SettingsView from '@/features/settings/views/SettingsView.vue';
import NotFound from '../views/NotFound.vue';
import NotFound from '@/views/NotFound.vue';
import { i18n } from '@/i18n';
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'startPage',
component: SearchView
component: SearchView,
},
{
path: '/search',
name: 'searchResults',
component: SearchResultsView,
props: route => ({
searchQuery: route.query.q
props: (route) => ({
searchQuery: route.query.q,
}),
meta: {
title: (route) => route.query.q
}
title: (route) => route.query.q,
},
},
{
path: '/legal/notice',
name: 'legalNotice',
component: LegalNoticeView,
meta: {
title: () => i18n.global.t('legal.notice.title')
}
title: () => i18n.global.t('legal.notice.title'),
},
},
{
path: '/settings/:rest(.*)*',
name: 'settings',
component: SettingsView,
meta: {
title: () => i18n.global.t('preferences.settings')
}
title: () => i18n.global.t('preferences.settings'),
},
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
component: NotFound
component: NotFound,
},
];
const router = createRouter({
history: createWebHistory(),
routes
routes,
});
export const updatePageTitle = function updatePageTitle (route) {
export const updatePageTitle = function updatePageTitle(route) {
const title =
typeof route.meta.title === 'function'
? route.meta.title(route)
: route.meta.title;
? route.meta.title(route)
: route.meta.title;
if (title) {
document.title = `${title} - Seekra`;
} else {
document.title = 'Seekra';
};
}
};
// remove trailing slash(es)
@@ -88,9 +88,9 @@ router.beforeEach((route) => {
path: route.path.replace(/\/+$/, ''),
query: route.query,
hash: route.hash,
replace: true
replace: true,
};
};
}
});
export default router;
export default router;
+15 -2
View File
@@ -19,7 +19,19 @@ body {
color: var(--dark);
margin: 0;
font-size: 16px;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-family:
'Inter',
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
}
.link {
@@ -27,7 +39,8 @@ body {
color: var(--dark);
}
.link:hover:not(.button-link), .link:focus-visible:not(.button-link) {
.link:hover:not(.button-link),
.link:focus-visible:not(.button-link) {
outline: none;
text-decoration: underline;
}
+107 -107
View File
@@ -15,144 +15,144 @@ limitations under the License.
*/
body {
--primary-color-l-8: oklch(0.897 0.202 260);
--primary-color-l-7: oklch(0.847 0.202 260);
--primary-color-l-6: oklch(0.797 0.202 260);
--primary-color-l-6: oklch(0.747 0.202 260);
--primary-color-l-4: oklch(0.697 0.202 260);
--primary-color-l-3: oklch(0.647 0.202 260);
--primary-color-l-2: oklch(0.597 0.202 260);
--primary-color-l-1: oklch(0.547 0.202 260);
--primary-color: oklch(0.497 0.202 260);
--primary-color-d-1: oklch(0.447 0.202 260);
--primary-color-d-2: oklch(0.397 0.202 260);
--primary-color-d-3: oklch(0.347 0.202 260);
--primary-color-d-4: oklch(0.297 0.202 260);
--primary-color-d-5: oklch(0.247 0.202 260);
--primary-color-d-6: oklch(0.197 0.202 260);
--primary-color-d-7: oklch(0.147 0.202 260);
--primary-color-d-8: oklch(0.097 0.202 260);
--primary-color-l-8: oklch(0.897 0.202 260);
--primary-color-l-7: oklch(0.847 0.202 260);
--primary-color-l-6: oklch(0.797 0.202 260);
--primary-color-l-6: oklch(0.747 0.202 260);
--primary-color-l-4: oklch(0.697 0.202 260);
--primary-color-l-3: oklch(0.647 0.202 260);
--primary-color-l-2: oklch(0.597 0.202 260);
--primary-color-l-1: oklch(0.547 0.202 260);
--primary-color: oklch(0.497 0.202 260);
--primary-color-d-1: oklch(0.447 0.202 260);
--primary-color-d-2: oklch(0.397 0.202 260);
--primary-color-d-3: oklch(0.347 0.202 260);
--primary-color-d-4: oklch(0.297 0.202 260);
--primary-color-d-5: oklch(0.247 0.202 260);
--primary-color-d-6: oklch(0.197 0.202 260);
--primary-color-d-7: oklch(0.147 0.202 260);
--primary-color-d-8: oklch(0.097 0.202 260);
--black-l-8: oklch(0.4 0 0);
--black-l-7: oklch(0.35 0 0);
--black-l-6: oklch(0.3 0 0);
--black-l-5: oklch(0.25 0 0);
--black-l-4: oklch(0.2 0 0);
--black-l-3: oklch(0.15 0 0);
--black-l-2: oklch(0.1 0 0);
--black-l-1: oklch(0.05 0 0);
--black: oklch(0 0 0);
--black-l-8: oklch(0.4 0 0);
--black-l-7: oklch(0.35 0 0);
--black-l-6: oklch(0.3 0 0);
--black-l-5: oklch(0.25 0 0);
--black-l-4: oklch(0.2 0 0);
--black-l-3: oklch(0.15 0 0);
--black-l-2: oklch(0.1 0 0);
--black-l-1: oklch(0.05 0 0);
--black: oklch(0 0 0);
--white: oklch(1 0 0);
--white-d-1: oklch(0.95 0 0);
--white-d-2: oklch(0.9 0 0);
--white-d-3: oklch(0.85 0 0);
--white-d-4: oklch(0.8 0 0);
--white-d-5: oklch(0.75 0 0);
--white-d-6: oklch(0.7 0 0);
--white-d-7: oklch(0.65 0 0);
--white-d-8: oklch(0.6 0 0);
--white: oklch(1 0 0);
--white-d-1: oklch(0.95 0 0);
--white-d-2: oklch(0.9 0 0);
--white-d-3: oklch(0.85 0 0);
--white-d-4: oklch(0.8 0 0);
--white-d-5: oklch(0.75 0 0);
--white-d-6: oklch(0.7 0 0);
--white-d-7: oklch(0.65 0 0);
--white-d-8: oklch(0.6 0 0);
--dark-l-8: var(--black-l-8);
--dark-l-7: var(--black-l-7);
--dark-l-6: var(--black-l-6);
--dark-l-5: var(--black-l-5);
--dark-l-4: var(--black-l-4);
--dark-l-3: var(--black-l-3);
--dark-l-2: var(--black-l-2);
--dark-l-1: var(--black-l-1);
--dark: var(--black);
--dark-l-8: var(--black-l-8);
--dark-l-7: var(--black-l-7);
--dark-l-6: var(--black-l-6);
--dark-l-5: var(--black-l-5);
--dark-l-4: var(--black-l-4);
--dark-l-3: var(--black-l-3);
--dark-l-2: var(--black-l-2);
--dark-l-1: var(--black-l-1);
--dark: var(--black);
--light: var(--white);
--light-d-1: var(--white-d-1);
--light-d-2: var(--white-d-2);
--light-d-3: var(--white-d-3);
--light-d-4: var(--white-d-4);
--light-d-5: var(--white-d-5);
--light-d-6: var(--white-d-6);
--light-d-7: var(--white-d-7);
--light-d-8: var(--white-d-8);
--light: var(--white);
--light-d-1: var(--white-d-1);
--light-d-2: var(--white-d-2);
--light-d-3: var(--white-d-3);
--light-d-4: var(--white-d-4);
--light-d-5: var(--white-d-5);
--light-d-6: var(--white-d-6);
--light-d-7: var(--white-d-7);
--light-d-8: var(--white-d-8);
--dark-bg: var(--black-l-2);
--light-bg: var(--white);
--dark-bg: var(--black-l-2);
--light-bg: var(--white);
--border: var(--white-d-3);
--border: var(--white-d-3);
--gray-box-shadow: oklch(0.8 0.0001 271 / 0.7);
--blue-box-shadow: oklch(0.52 0.15 268 / 0.25);
--gray-box-shadow: oklch(0.8 0.0001 271 / 0.7);
--blue-box-shadow: oklch(0.52 0.15 268 / 0.25);
--light-hover: var(--light-d-2);
--light-hover: var(--light-d-2);
--invert: invert(0);
--color-negative-bg: oklch(59% 0.22706 29.244);
--color-negative-bg: oklch(59% 0.22706 29.244);
--color-negative-border: oklch(51% 0.22706 29.244);
}
@media (prefers-color-scheme: dark) {
body.color-scheme-auto {
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
--dark-l-5: var(--white-d-5);
--dark-l-4: var(--white-d-4);
--dark-l-3: var(--white-d-3);
--dark-l-2: var(--white-d-2);
--dark-l-1: var(--white-d-1);
--dark: var(--white);
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
--dark-l-5: var(--white-d-5);
--dark-l-4: var(--white-d-4);
--dark-l-3: var(--white-d-3);
--dark-l-2: var(--white-d-2);
--dark-l-1: var(--white-d-1);
--dark: var(--white);
--light: var(--black);
--light-d-1: oklch(0.10 0 0);
--light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4);
--light-d-5: var(--black-l-5);
--light-d-6: var(--black-l-6);
--light-d-7: var(--black-l-7);
--light-d-8: var(--black-l-8);
--light: var(--black);
--light-d-1: oklch(0.1 0 0);
--light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4);
--light-d-5: var(--black-l-5);
--light-d-6: var(--black-l-6);
--light-d-7: var(--black-l-7);
--light-d-8: var(--black-l-8);
--dark-bg: var(--white);
--light-bg: oklch(0.18 0 0);
--dark-bg: var(--white);
--light-bg: oklch(0.18 0 0);
--border: var(--black-l-6);
--border: var(--black-l-6);
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--light-hover: var(--light-d-5);
--light-hover: var(--light-d-5);
--invert: invert(1);
}
}
body.dark {
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
--dark-l-5: var(--white-d-5);
--dark-l-4: var(--white-d-4);
--dark-l-3: var(--white-d-3);
--dark-l-2: var(--white-d-2);
--dark-l-1: var(--white-d-1);
--dark: var(--white);
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
--dark-l-5: var(--white-d-5);
--dark-l-4: var(--white-d-4);
--dark-l-3: var(--white-d-3);
--dark-l-2: var(--white-d-2);
--dark-l-1: var(--white-d-1);
--dark: var(--white);
--light: var(--black);
--light-d-1: oklch(0.10 0 0);
--light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4);
--light-d-5: var(--black-l-5);
--light-d-6: var(--black-l-6);
--light-d-7: var(--black-l-7);
--light-d-8: var(--black-l-8);
--light: var(--black);
--light-d-1: oklch(0.1 0 0);
--light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4);
--light-d-5: var(--black-l-5);
--light-d-6: var(--black-l-6);
--light-d-7: var(--black-l-7);
--light-d-8: var(--black-l-8);
--dark-bg: var(--white);
--light-bg: oklch(0.18 0 0);
--dark-bg: var(--white);
--light-bg: oklch(0.18 0 0);
--border: var(--black-l-6);
--border: var(--black-l-6);
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--light-hover: var(--light-d-5);
--light-hover: var(--light-d-5);
--invert: invert(1);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
:root {
--input-padding: 0.7em;
--input-height: 1em;
}
}
+16 -17
View File
@@ -15,34 +15,37 @@ limitations under the License.
*/
import { createTestingPinia } from '@pinia/testing';
import { vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { vi } from 'vitest';
import { createI18n } from 'vue-i18n';
import { createMemoryHistory, createRouter } from 'vue-router';
export const mountComponent = function mountComponent (component, options = {}, i18nMessages = {}, routes = [], piniaOptions = {}) {
const {
setupStores,
...testingPiniaOptions
} = piniaOptions;
export const mountComponent = function mountComponent(
component,
options = {},
i18nMessages = {},
routes = [],
piniaOptions = {},
) {
const { setupStores, ...testingPiniaOptions } = piniaOptions;
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: i18nMessages
}
en: i18nMessages,
},
});
const router = createRouter({
history: createMemoryHistory(),
routes
routes,
});
const testingPinia = createTestingPinia({
...testingPiniaOptions,
stubActions: false,
createSpy: vi.fn
createSpy: vi.fn,
});
if (setupStores) {
@@ -51,12 +54,8 @@ export const mountComponent = function mountComponent (component, options = {},
return mount(component, {
global: {
plugins: [
i18n,
router,
testingPinia
],
plugins: [i18n, router, testingPinia],
},
...options
...options,
});
};
};
+102 -90
View File
@@ -14,111 +14,123 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { describe, expect, test } from 'vitest';
import { ensureUnit } from '../cssDimensions';
import { describe, expect, test } from 'vitest';
describe('cssDimensions', () => {
test.for([
{ dimension: null, expected: '0px' },
{ dimension: undefined, expected: '0px' },
{ 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, 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.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.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', 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.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: '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: '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.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: '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: '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.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: '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: '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);
});
});
{ 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);
},
);
});
+3 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export const ensureUnit = function (d) {
export const ensureUnit = function ensureUnit(d) {
if (!d) d = 0;
// ensure that it is a string
@@ -25,7 +25,7 @@ export const ensureUnit = function (d) {
// if it ends with a number
if (/\d$/.test(d)) {
d += 'px';
};
}
return d;
};
};
+3 -3
View File
@@ -22,7 +22,7 @@ const { t } = useI18n();
<template>
<div class="not-found main-content-padding">
<span class="error-message">
<span class="error-message">
{{ t('error.pageNotFound') }}
</span>
<RouterLink to="/" id="link">
@@ -40,7 +40,7 @@ const { t } = useI18n();
align-items: center;
gap: 12px;
}
.error-message{
.error-message {
margin: 0;
font-weight: 600;
font-size: 1.75rem;
@@ -53,4 +53,4 @@ const { t } = useI18n();
padding: 10px 20px;
text-decoration: none;
}
</style>
</style>
+21 -21
View File
@@ -14,29 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vue from '@vitejs/plugin-vue';
import path from 'path';
import { defineConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
// https://vite.dev/config/
export default defineConfig({
build: {
target: 'es2020'
},
plugins: [
vue(),
createHtmlPlugin({
minify: true
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
test: {
globals: false,
environment: 'jsdom'
}
})
build: {
target: 'es2020',
},
plugins: [
vue(),
createHtmlPlugin({
minify: true,
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
globals: false,
environment: 'jsdom',
},
});