Style: make code style consistent #186
@@ -42,4 +42,4 @@ jobs:
|
|||||||
|
|
||||||
printf "Update current build pointer ... "
|
printf "Update current build pointer ... "
|
||||||
echo "$BUILD_ID" | mc pipe "minio/$BUCKET/current"
|
echo "$BUILD_ID" | mc pipe "minio/$BUCKET/current"
|
||||||
echo "done"
|
echo "done"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.gitea/
|
||||||
|
.github/
|
||||||
|
eslint.config.js
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
@@ -5,4 +5,4 @@ This software is provided "as is", without warranty of any kind.
|
|||||||
This software uses the Vue.js framework.
|
This software uses the Vue.js framework.
|
||||||
|
|
||||||
This software makes use of various open-source components.
|
This software makes use of various open-source components.
|
||||||
See THIRD_PARTY_LICENSES for details.
|
See THIRD_PARTY_LICENSES for details.
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ Please execute the setup script in `scripts/` for your platform when cloning the
|
|||||||
|
|
||||||
## Legal
|
## 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.
|
||||||
|
|||||||
@@ -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:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- @jridgewell/resolve-uri@3.1.2
|
- @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:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- birpc@2.9.0
|
- 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:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- unplugin@3.0.0
|
- unplugin@3.0.0
|
||||||
@@ -683,9 +798,12 @@ SOFTWARE.
|
|||||||
|
|
||||||
The following npm packages may be included in this product:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @vue/devtools-api@8.1.1
|
- @vue/devtools-api@7.7.10
|
||||||
- @vue/devtools-kit@8.1.1
|
- @vue/devtools-api@8.1.5
|
||||||
- @vue/devtools-shared@8.1.1
|
- @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:
|
These packages each contain the following license:
|
||||||
|
|
||||||
@@ -715,7 +833,7 @@ SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- tinyglobby@0.2.16
|
- tinyglobby@0.2.17
|
||||||
|
|
||||||
This package contains the following license:
|
This package contains the following license:
|
||||||
|
|
||||||
@@ -775,7 +893,9 @@ SOFTWARE.
|
|||||||
|
|
||||||
The following npm packages may be included in this product:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
|
- defu@6.1.7
|
||||||
- mlly@1.8.2
|
- mlly@1.8.2
|
||||||
|
- perfect-debounce@1.0.0
|
||||||
- perfect-debounce@2.1.0
|
- perfect-debounce@2.1.0
|
||||||
- scule@1.3.0
|
- scule@1.3.0
|
||||||
- ufo@1.6.4
|
- 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:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- chokidar@5.0.0
|
- chokidar@5.0.0
|
||||||
@@ -1284,7 +1455,7 @@ SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- picomatch@4.0.4
|
- picomatch@4.0.5
|
||||||
|
|
||||||
This package contains the following license:
|
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
|
- vue-router@5.0.6
|
||||||
|
|
||||||
This package contains the following license:
|
These packages each contain the following license:
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
@@ -1587,7 +1759,7 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- postcss@8.5.14
|
- postcss@8.5.26
|
||||||
|
|
||||||
This package contains the following license:
|
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:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- nanoid@3.3.12
|
- nanoid@3.3.18
|
||||||
|
|
||||||
This package contains the following license:
|
This package contains the following license:
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,4 @@
|
|||||||
- Internationalization and some major languages
|
- Internationalization and some major languages
|
||||||
- Color scheme
|
- Color scheme
|
||||||
- Search results view with error message which indicates that the search is not available
|
- Search results view with error message which indicates that the search is not available
|
||||||
- Settings renderer
|
- Settings renderer
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
]);
|
||||||
@@ -24,7 +24,19 @@ limitations under the License.
|
|||||||
<noscript>
|
<noscript>
|
||||||
<style>
|
<style>
|
||||||
body {
|
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 {
|
a {
|
||||||
@@ -40,17 +52,15 @@ limitations under the License.
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<h1>
|
<h1>Seekra</h1>
|
||||||
Seekra
|
<p>You need to enable JavaScript to use Seekra.</p>
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
You need to enable JavaScript to use Seekra.
|
|
||||||
</p>
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Legal Content</h2>
|
<h2>Legal Content</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<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
|
Legal Notice
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run"
|
"test:run": "vitest run",
|
||||||
},
|
"lint": "eslint .",
|
||||||
"dependencies": {
|
"lint:fix": "eslint . --fix",
|
||||||
"pinia": "^3.0.4",
|
"format": "prettier . --write",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"format:check": "prettier . --check"
|
||||||
"terser": "^5.47.1",
|
},
|
||||||
"vue": "^3.5.32",
|
"dependencies": {
|
||||||
"vue-i18n": "^11.4.4",
|
"pinia": "^3.0.4",
|
||||||
"vue-router": "^5.0.6"
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
},
|
"terser": "^5.47.1",
|
||||||
"devDependencies": {
|
"vue": "^3.5.32",
|
||||||
"@pinia/testing": "^1.0.3",
|
"vue-i18n": "^11.4.4",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"vue-router": "^5.0.6"
|
||||||
"@vue/test-utils": "^2.4.6",
|
},
|
||||||
"jsdom": "^29.1.1",
|
"devDependencies": {
|
||||||
"vite": "^8.0.10",
|
"@eslint/js": "^10.0.1",
|
||||||
"vite-plugin-html": "^3.2.2",
|
"@pinia/testing": "^1.0.3",
|
||||||
"vitest": "^4.1.7"
|
"@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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,21 +22,16 @@ import { generateLicenseFile } from 'generate-license-file';
|
|||||||
const packageJson = './package.json';
|
const packageJson = './package.json';
|
||||||
const packageJsonBackup = `${packageJson}~`;
|
const packageJsonBackup = `${packageJson}~`;
|
||||||
|
|
||||||
await generateLicenseFile(
|
await generateLicenseFile(packageJson, 'THIRD_PARTY_LICENSES');
|
||||||
packageJson,
|
|
||||||
'THIRD_PARTY_LICENSES'
|
|
||||||
);
|
|
||||||
|
|
||||||
const pkg = JSON.parse(
|
const pkg = JSON.parse(await fs.readFile(packageJson, 'utf-8'));
|
||||||
await fs.readFile(packageJson, 'utf-8')
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.rename(packageJson, packageJsonBackup);
|
await fs.rename(packageJson, packageJsonBackup);
|
||||||
|
|
||||||
const devPackage = {
|
const devPackage = {
|
||||||
...pkg,
|
...pkg,
|
||||||
dependencies: pkg.devDependencies ?? {},
|
dependencies: pkg.devDependencies ?? {},
|
||||||
devDependencies: {}
|
devDependencies: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -45,14 +40,8 @@ const devPackage = {
|
|||||||
|
|
||||||
https://github.com/TobyAndToby/generate-license-file/issues/752
|
https://github.com/TobyAndToby/generate-license-file/issues/752
|
||||||
*/
|
*/
|
||||||
await fs.writeFile(
|
await fs.writeFile(packageJson, JSON.stringify(devPackage));
|
||||||
packageJson,
|
|
||||||
JSON.stringify(devPackage)
|
|
||||||
);
|
|
||||||
|
|
||||||
await generateLicenseFile(
|
await generateLicenseFile(packageJson, './THIRD_PARTY_LICENSES_DEV');
|
||||||
packageJson,
|
|
||||||
'./THIRD_PARTY_LICENSES_DEV'
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.rename(packageJsonBackup, packageJson);
|
await fs.rename(packageJsonBackup, packageJson);
|
||||||
|
|||||||
@@ -14,4 +14,4 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
git config --local commit.template .gitmessage
|
git config --local commit.template .gitmessage
|
||||||
|
|||||||
@@ -14,4 +14,4 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
git config --local commit.template .gitmessage
|
git config --local commit.template .gitmessage
|
||||||
|
|||||||
@@ -15,13 +15,12 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<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 { 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 { useSettings } from './features/settings/composables/useSettings.js';
|
||||||
import { setLocale } from './i18n';
|
import { setLocale } from './i18n';
|
||||||
|
import { updatePageTitle } from './router';
|
||||||
import { computed, onMounted, watch, watchEffect } from 'vue';
|
import { computed, onMounted, watch, watchEffect } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
@@ -30,11 +29,11 @@ const route = useRoute();
|
|||||||
const { getSetting } = useSettings();
|
const { getSetting } = useSettings();
|
||||||
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
|
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
|
||||||
|
|
||||||
const updateColorScheme = function updateColorScheme (newColorScheme) {
|
const updateColorScheme = function updateColorScheme(newColorScheme) {
|
||||||
document.body.style.setProperty(colorScheme, {
|
document.body.style.setProperty(colorScheme, {
|
||||||
auto: 'normal',
|
auto: 'normal',
|
||||||
dark: 'dark',
|
dark: 'dark',
|
||||||
light: 'light'
|
light: 'light',
|
||||||
});
|
});
|
||||||
if (newColorScheme === 'dark') {
|
if (newColorScheme === 'dark') {
|
||||||
document.body.classList.add('dark');
|
document.body.classList.add('dark');
|
||||||
@@ -46,7 +45,7 @@ const updateColorScheme = function updateColorScheme (newColorScheme) {
|
|||||||
} else {
|
} else {
|
||||||
document.body.classList.remove('color-scheme-auto');
|
document.body.classList.remove('color-scheme-auto');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
watch(colorScheme, (newColorScheme) => {
|
watch(colorScheme, (newColorScheme) => {
|
||||||
updateColorScheme(newColorScheme);
|
updateColorScheme(newColorScheme);
|
||||||
});
|
});
|
||||||
@@ -61,11 +60,11 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="app-wrapper">
|
<div id="app-wrapper">
|
||||||
<Navbar />
|
<AppNavbar />
|
||||||
|
|
||||||
<router-view class="main-content" />
|
<router-view class="main-content" />
|
||||||
|
|
||||||
<Footer />
|
<AppFooter />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -73,7 +72,8 @@ onMounted(() => {
|
|||||||
#app-wrapper {
|
#app-wrapper {
|
||||||
--main-content-padding-x: 30px;
|
--main-content-padding-x: 30px;
|
||||||
--main-content-padding-y: 40px;
|
--main-content-padding-y: 40px;
|
||||||
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x);
|
--main-content-padding: var(--main-content-padding-y)
|
||||||
|
var(--main-content-padding-x);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -90,4 +90,4 @@ onMounted(() => {
|
|||||||
--main-content-padding-x: 15px;
|
--main-content-padding-x: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ limitations under the License.
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M5 12 l6 8 l8 -16"
|
d="M5 12 l6 8 l8 -16"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 832 B After Width: | Height: | Size: 833 B |
@@ -23,4 +23,4 @@ limitations under the License.
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M4 8 l8 8 l8 -8"
|
d="M4 8 l8 8 l8 -8"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 830 B After Width: | Height: | Size: 831 B |
@@ -23,4 +23,4 @@ limitations under the License.
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M8 20 l8 -8 l-8 -8"
|
d="M8 20 l8 -8 l-8 -8"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 834 B |
@@ -35,4 +35,4 @@ limitations under the License.
|
|||||||
a9 9 0 0 0 -9 -9
|
a9 9 0 0 0 -9 -9
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -33,4 +33,4 @@ limitations under the License.
|
|||||||
c-2.140625 4.351562000000001 -6.480468999999999 7.058593000000002 -11.328125 7.058593000000002
|
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 |
@@ -33,4 +33,4 @@ limitations under the License.
|
|||||||
cy="8.5"
|
cy="8.5"
|
||||||
r="6.5"
|
r="6.5"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -35,4 +35,4 @@ limitations under the License.
|
|||||||
v16
|
v16
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -42,4 +42,4 @@ limitations under the License.
|
|||||||
|
|
||||||
<!-- bottom right -->
|
<!-- bottom right -->
|
||||||
<circle fill="currentColor" r="2" cx="18.36396103067893" cy="18.36396103067893" />
|
<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 |
@@ -16,4 +16,4 @@ limitations under the License.
|
|||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="758.75" height="192.5">
|
<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>
|
<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 |
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const getAssetsUrl = function getAssetsUrl () {
|
export const getAssetsUrl = function getAssetsUrl() {
|
||||||
return new URL(
|
return new URL(
|
||||||
`${import.meta.env.VITE_ASSETS_BUILD_ID}/`,
|
`${import.meta.env.VITE_ASSETS_BUILD_ID}/`,
|
||||||
import.meta.env.VITE_ASSETS_BASE_URL
|
import.meta.env.VITE_ASSETS_BASE_URL,
|
||||||
).href;
|
).href;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<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 { useSettings } from '@/features/settings/composables/useSettings';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -27,33 +26,36 @@ const { getSetting, setSetting } = useSettings();
|
|||||||
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
|
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
|
||||||
|
|
||||||
const colorSchemeNextMapper = {
|
const colorSchemeNextMapper = {
|
||||||
'light': 'dark',
|
light: 'dark',
|
||||||
'dark': 'auto',
|
dark: 'auto',
|
||||||
'auto': 'light'
|
auto: 'light',
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorSchemeIconMapper = {
|
const colorSchemeIconMapper = {
|
||||||
'dark': 'crescent-moon',
|
dark: 'crescent-moon',
|
||||||
'light': 'sun',
|
light: 'sun',
|
||||||
'auto': 'circle-black-white'
|
auto: 'circle-black-white',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTooltipTranslation = function (colorScheme) {
|
const getTooltipTranslation = function getTooltipTranslation(colorScheme) {
|
||||||
return t(`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`);
|
return t(
|
||||||
|
`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setColorScheme = function setColorScheme (newColorScheme) {
|
const setColorScheme = function setColorScheme(newColorScheme) {
|
||||||
setSetting(['appearance', 'colorScheme'], newColorScheme);
|
setSetting(['appearance', 'colorScheme'], newColorScheme);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button class="color-scheme-button"
|
<button
|
||||||
|
class="color-scheme-button"
|
||||||
@click="setColorScheme(colorSchemeNextMapper[colorScheme])"
|
@click="setColorScheme(colorSchemeNextMapper[colorScheme])"
|
||||||
:aria-label="getTooltipTranslation(colorScheme)"
|
:aria-label="getTooltipTranslation(colorScheme)"
|
||||||
:title="getTooltipTranslation(colorScheme)"
|
:title="getTooltipTranslation(colorScheme)"
|
||||||
>
|
>
|
||||||
<Icon
|
<UIIcon
|
||||||
:name="colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]]"
|
:name="colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]]"
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
@@ -78,4 +80,4 @@ const setColorScheme = function setColorScheme (newColorScheme) {
|
|||||||
.color-scheme-button:hover {
|
.color-scheme-button:hover {
|
||||||
background: var(--light-hover);
|
background: var(--light-hover);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import { describe, expect, test } from 'vitest';
|
|||||||
|
|
||||||
const { getSetting } = useSettings();
|
const { getSetting } = useSettings();
|
||||||
|
|
||||||
const colorSchemeSettingPath = ['appearance', 'colorScheme']
|
const colorSchemeSettingPath = ['appearance', 'colorScheme'];
|
||||||
|
|
||||||
const getWrapper = function getWrapper () {
|
const getWrapper = function getWrapper() {
|
||||||
return mountComponent(ColorSchemeButton);
|
return mountComponent(ColorSchemeButton);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ describe('ColorSchemeButton', () => {
|
|||||||
const wrapper = getWrapper();
|
const wrapper = getWrapper();
|
||||||
|
|
||||||
expect(getSetting(colorSchemeSettingPath)).toBe('auto');
|
expect(getSetting(colorSchemeSettingPath)).toBe('auto');
|
||||||
|
|
||||||
const colorSchemeButton = wrapper.find('button');
|
const colorSchemeButton = wrapper.find('button');
|
||||||
|
|
||||||
await colorSchemeButton.trigger('click');
|
await colorSchemeButton.trigger('click');
|
||||||
|
|||||||
@@ -16,20 +16,17 @@ limitations under the License.
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
|
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
|
||||||
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const startYear = 2026
|
const startYear = 2026;
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const copyrightPeriod =
|
const copyrightPeriod =
|
||||||
startYear === currentYear
|
startYear === currentYear
|
||||||
? `${currentYear}`
|
? `${currentYear}`
|
||||||
: `${startYear}-${currentYear}`
|
: `${startYear}-${currentYear}`;
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -103,4 +100,4 @@ const copyrightPeriod =
|
|||||||
margin-top: 0.8em;
|
margin-top: 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -15,13 +15,12 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Icon from '@/features/icons/components/Icon.vue';
|
|
||||||
|
|
||||||
import { LANGUAGE_PATH } from '@/config/featureSettings';
|
import { LANGUAGE_PATH } from '@/config/featureSettings';
|
||||||
|
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||||
import { useSettings } from '@/features/settings/composables/useSettings';
|
import { useSettings } from '@/features/settings/composables/useSettings';
|
||||||
|
import { setLocale, SUPPORTED_LANGUAGES } from '@/i18n';
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { setLocale, SUPPORTED_LANGUAGES } from '@/i18n';
|
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const { setSetting, getSetting } = useSettings();
|
const { setSetting, getSetting } = useSettings();
|
||||||
@@ -29,33 +28,33 @@ const { setSetting, getSetting } = useSettings();
|
|||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
const languageDropdown = ref(null);
|
const languageDropdown = ref(null);
|
||||||
|
|
||||||
const selectLanguage = function selectLanguage (code) {
|
const selectLanguage = function selectLanguage(code) {
|
||||||
setSetting(LANGUAGE_PATH, code);
|
setSetting(LANGUAGE_PATH, code);
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => getSetting(LANGUAGE_PATH), setLocale);
|
watch(() => getSetting(LANGUAGE_PATH), setLocale);
|
||||||
|
|
||||||
const close = function () {
|
const close = function close() {
|
||||||
document.removeEventListener('click', closeWrapperOnClickOutsite);
|
document.removeEventListener('click', closeWrapperOnClickOutside);
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeWrapperOnClickOutsite = function (e) {
|
const closeWrapperOnClickOutside = function closeWrapperOnClickOutside(e) {
|
||||||
if (languageDropdown.value) {
|
if (languageDropdown.value) {
|
||||||
if (!languageDropdown.value.contains(e.target)) {
|
if (!languageDropdown.value.contains(e.target)) {
|
||||||
close();
|
close();
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = function () {
|
const open = function open() {
|
||||||
if (!isOpen.value) {
|
if (!isOpen.value) {
|
||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.addEventListener('click', closeWrapperOnClickOutsite);
|
document.addEventListener('click', closeWrapperOnClickOutside);
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -67,9 +66,11 @@ const open = function () {
|
|||||||
:aria-expanded="isOpen"
|
:aria-expanded="isOpen"
|
||||||
aria-haspopup="listbox"
|
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 }">
|
<span class="chevron" :class="{ open: isOpen }">
|
||||||
<Icon name="chevron-down" size="1em" />
|
<UIIcon name="chevron-down" size="1em" />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ const open = function () {
|
|||||||
class="language-dropdown"
|
class="language-dropdown"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
:style="{
|
: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
|
<li
|
||||||
@@ -90,7 +91,9 @@ const open = function () {
|
|||||||
:class="{ active: lang === locale }"
|
:class="{ active: lang === locale }"
|
||||||
@click="selectLanguage(lang)"
|
@click="selectLanguage(lang)"
|
||||||
>
|
>
|
||||||
<span class="lang-label">{{ t(`preferences.locale.languages.${lang}`) }}</span>
|
<span class="lang-label">{{
|
||||||
|
t(`preferences.locale.languages.${lang}`)
|
||||||
|
}}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,4 +166,4 @@ const open = function () {
|
|||||||
.flag {
|
.flag {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,31 +14,33 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
|
||||||
import { LANGUAGE_PATH } from '@/config/featureSettings.js';
|
import { LANGUAGE_PATH } from '@/config/featureSettings.js';
|
||||||
import { useSettings } from '@/features/settings/composables/useSettings.js';
|
import { useSettings } from '@/features/settings/composables/useSettings.js';
|
||||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||||
import { expect, test, describe } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
|
|
||||||
|
|
||||||
const getWrapper = function getWrapper () {
|
const getWrapper = function getWrapper() {
|
||||||
return mountComponent(LanguageSwitchButton);
|
return mountComponent(LanguageSwitchButton);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('LanguageSwitchButton', () => {
|
describe('LanguageSwitchButton', () => {
|
||||||
test('renders correctly with initial state closed', () => {
|
test('renders correctly with initial state closed', () => {
|
||||||
const wrapper = getWrapper();
|
const wrapper = getWrapper();
|
||||||
|
|
||||||
expect(wrapper.find('.language-button').exists()).toBe(true);
|
expect(wrapper.find('.language-button').exists()).toBe(true);
|
||||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
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 () => {
|
test('opens the dropdown when language button is clicked', async () => {
|
||||||
const wrapper = getWrapper();
|
const wrapper = getWrapper();
|
||||||
const button = wrapper.find('.language-button');
|
const button = wrapper.find('.language-button');
|
||||||
|
|
||||||
await button.trigger('click');
|
await button.trigger('click');
|
||||||
|
|
||||||
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
|
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
|
||||||
expect(button.attributes('aria-expanded')).toBe('true');
|
expect(button.attributes('aria-expanded')).toBe('true');
|
||||||
});
|
});
|
||||||
@@ -47,17 +49,17 @@ describe('LanguageSwitchButton', () => {
|
|||||||
const { getSetting } = useSettings();
|
const { getSetting } = useSettings();
|
||||||
|
|
||||||
const wrapper = getWrapper();
|
const wrapper = getWrapper();
|
||||||
|
|
||||||
await wrapper.find('.language-button').trigger('click');
|
await wrapper.find('.language-button').trigger('click');
|
||||||
|
|
||||||
const options = wrapper.findAll('.language-dropdown li');
|
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');
|
await enOption.trigger('click');
|
||||||
expect(getSetting(LANGUAGE_PATH)).toBe('en');
|
expect(getSetting(LANGUAGE_PATH)).toBe('en');
|
||||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
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');
|
await deOption.trigger('click');
|
||||||
expect(getSetting(LANGUAGE_PATH)).toBe('de');
|
expect(getSetting(LANGUAGE_PATH)).toBe('de');
|
||||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||||
@@ -69,14 +71,14 @@ describe('LanguageSwitchButton', () => {
|
|||||||
await wrapper.find('.language-button').trigger('click');
|
await wrapper.find('.language-button').trigger('click');
|
||||||
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
|
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');
|
const externalDiv = document.createElement('div');
|
||||||
document.body.appendChild(externalDiv);
|
document.body.appendChild(externalDiv);
|
||||||
|
|
||||||
const clickEvent = new MouseEvent('click', { bubbles: true });
|
const clickEvent = new MouseEvent('click', { bubbles: true });
|
||||||
externalDiv.dispatchEvent(clickEvent);
|
externalDiv.dispatchEvent(clickEvent);
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||||
@@ -84,4 +86,4 @@ describe('LanguageSwitchButton', () => {
|
|||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
externalDiv.remove();
|
externalDiv.remove();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,33 +19,36 @@ import { describe, expect, test } from 'vitest';
|
|||||||
|
|
||||||
describe('navigatorLanguage', () => {
|
describe('navigatorLanguage', () => {
|
||||||
test.for([
|
test.for([
|
||||||
{ navigatorLanguage: 'en', expected: 'en'},
|
{ navigatorLanguage: 'en', expected: 'en' },
|
||||||
{ navigatorLanguage: 'de', expected: 'de'},
|
{ navigatorLanguage: 'de', expected: 'de' },
|
||||||
{ navigatorLanguage: 'fr', expected: 'fr'},
|
{ navigatorLanguage: 'fr', expected: 'fr' },
|
||||||
|
|
||||||
{ navigatorLanguage: 'en-US', expected: 'en'},
|
{ navigatorLanguage: 'en-US', expected: 'en' },
|
||||||
{ navigatorLanguage: 'en-AU', expected: 'en'},
|
{ navigatorLanguage: 'en-AU', expected: 'en' },
|
||||||
{ navigatorLanguage: 'de-DE', expected: 'de'},
|
{ navigatorLanguage: 'de-DE', expected: 'de' },
|
||||||
{ navigatorLanguage: 'fr-FR', expected: 'fr'},
|
{ navigatorLanguage: 'fr-FR', expected: 'fr' },
|
||||||
|
|
||||||
{ navigatorLanguage: 'en-us', expected: 'en'},
|
{ navigatorLanguage: 'en-us', expected: 'en' },
|
||||||
{ navigatorLanguage: 'en-au', expected: 'en'},
|
{ navigatorLanguage: 'en-au', expected: 'en' },
|
||||||
{ navigatorLanguage: 'de-de', expected: 'de'},
|
{ navigatorLanguage: 'de-de', expected: 'de' },
|
||||||
{ navigatorLanguage: 'fr-fr', expected: 'fr'},
|
{ navigatorLanguage: 'fr-fr', expected: 'fr' },
|
||||||
|
|
||||||
{ navigatorLanguage: 'zh-Hans-CN', expected: 'zh'},
|
{ navigatorLanguage: 'zh-Hans-CN', expected: 'zh' },
|
||||||
{ navigatorLanguage: 'zh-Hant-TW', expected: 'zh'},
|
{ navigatorLanguage: 'zh-Hant-TW', expected: 'zh' },
|
||||||
{ navigatorLanguage: 'uz-Latn-UZ', expected: 'uz'},
|
{ navigatorLanguage: 'uz-Latn-UZ', expected: 'uz' },
|
||||||
|
|
||||||
{ navigatorLanguage: 'en-US-u-ca-gregory', expected: 'en'},
|
{ navigatorLanguage: 'en-US-u-ca-gregory', expected: 'en' },
|
||||||
{ navigatorLanguage: 'de-DE-u-co-phonebk', expected: 'de'},
|
{ navigatorLanguage: 'de-DE-u-co-phonebk', expected: 'de' },
|
||||||
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', expected: 'zh'}
|
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', expected: 'zh' },
|
||||||
])('returns language $expected with navigator language $navigatorLanguage', ({ navigatorLanguage, expected }) => {
|
])(
|
||||||
Object.defineProperty(navigator, 'language', {
|
'returns language $expected with navigator language $navigatorLanguage',
|
||||||
value: navigatorLanguage,
|
({ navigatorLanguage, expected }) => {
|
||||||
configurable: true
|
Object.defineProperty(navigator, 'language', {
|
||||||
});
|
value: navigatorLanguage,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(getNavigatorLanguage()).toBe(expected);
|
expect(getNavigatorLanguage()).toBe(expected);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const getNavigatorLanguage = function getNavigatorLanguage () {
|
export const getNavigatorLanguage = function getNavigatorLanguage() {
|
||||||
const locale = new Intl.Locale(navigator.language);
|
const locale = new Intl.Locale(navigator.language);
|
||||||
return locale.language;
|
return locale.language;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,33 +15,33 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
|
||||||
import { ensureUnit } from '@/utils/cssDimensions';
|
import { ensureUnit } from '@/utils/cssDimensions';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: {
|
name: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: 24
|
default: 24,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const icons = import.meta.glob('@/assets/icons/*.svg', {
|
const icons = import.meta.glob('@/assets/icons/*.svg', {
|
||||||
eager: true,
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<img :src="Icon" :style="{ height: `${size}`, width: `${size}` }" />
|
<img :src="icon" :style="{ height: `${size}`, width: `${size}` }" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -51,4 +51,4 @@ const size = computed(() => ensureUnit(props.size))
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
filter: var(--invert);
|
filter: var(--invert);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -15,8 +15,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export class FetchError extends Error {
|
export class FetchError extends Error {
|
||||||
constructor (message) {
|
constructor(message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'FetchError';
|
this.name = 'FetchError';
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -60,29 +60,25 @@ describe('fetchLegalContent', () => {
|
|||||||
describe('getLegalNotice', () => {
|
describe('getLegalNotice', () => {
|
||||||
describe('success', () => {
|
describe('success', () => {
|
||||||
test('returns HTML body', async () => {
|
test('returns HTML body', async () => {
|
||||||
fetchSpy.mockResolvedValue(
|
fetchSpy.mockResolvedValue(new Response(exampleHtmlDocument));
|
||||||
new Response(exampleHtmlDocument)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect((await getLegalNotice()).trim()).toBe(
|
||||||
(await getLegalNotice()).trim()
|
exampleHtmlWithLinkClass,
|
||||||
).toBe(exampleHtmlWithLinkClass);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns HTML <main>', async () => {
|
test('returns HTML <main>', async () => {
|
||||||
fetchSpy.mockResolvedValue(
|
fetchSpy.mockResolvedValue(
|
||||||
new Response(exampleHtmlDocumentWithMain)
|
new Response(exampleHtmlDocumentWithMain),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect((await getLegalNotice()).trim()).toBe(
|
||||||
(await getLegalNotice()).trim()
|
exampleHtmlWithLinkClass,
|
||||||
).toBe(exampleHtmlWithLinkClass);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not fail on success status code', async () => {
|
test('does not fail on success status code', async () => {
|
||||||
fetchSpy.mockResolvedValue(
|
fetchSpy.mockResolvedValue(new Response(null, { status: 206 }));
|
||||||
new Response(null, { status: 206 })
|
|
||||||
);
|
|
||||||
|
|
||||||
await getLegalNotice();
|
await getLegalNotice();
|
||||||
});
|
});
|
||||||
@@ -95,9 +91,7 @@ describe('fetchLegalContent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('throws FetchError on HTTP error', async () => {
|
test('throws FetchError on HTTP error', async () => {
|
||||||
fetchSpy.mockResolvedValue(
|
fetchSpy.mockResolvedValue(new Response(null, { status: 404 }));
|
||||||
new Response(null, { status: 404 })
|
|
||||||
)
|
|
||||||
await expect(getLegalNotice()).rejects.toThrow(FetchError);
|
await expect(getLegalNotice()).rejects.toThrow(FetchError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import { FetchError } from '../errors';
|
import { FetchError } from '../errors';
|
||||||
import { getAssetsUrl } from '@/config/env';
|
import { getAssetsUrl } from '@/config/env';
|
||||||
|
|
||||||
const fetchHtml = async function fetchHtml (url, options) {
|
const fetchHtml = async function fetchHtml(url, options) {
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(url, options);
|
response = await fetch(url, options);
|
||||||
@@ -29,16 +29,17 @@ const fetchHtml = async function fetchHtml (url, options) {
|
|||||||
else throw new FetchError();
|
else throw new FetchError();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLegalNotice = async function getLegalNotice () {
|
export const getLegalNotice = async function getLegalNotice() {
|
||||||
const legalNoticeUrl = new URL('legal/legal_notice.html', getAssetsUrl()).href;
|
const legalNoticeUrl = new URL('legal/legal_notice.html', getAssetsUrl())
|
||||||
|
.href;
|
||||||
const html = await fetchHtml(legalNoticeUrl);
|
const html = await fetchHtml(legalNoticeUrl);
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
const legalNoticeContainer = doc.querySelector('main') ?? doc.body;
|
const legalNoticeContainer = doc.querySelector('main') ?? doc.body;
|
||||||
legalNoticeContainer.querySelectorAll('a').forEach(
|
legalNoticeContainer
|
||||||
(link) => link.classList.add('link')
|
.querySelectorAll('a')
|
||||||
);
|
.forEach((link) => link.classList.add('link'));
|
||||||
return legalNoticeContainer.innerHTML;
|
return legalNoticeContainer.innerHTML;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,16 +20,18 @@ import { mountComponent } from '@/test-utils/mountComponent';
|
|||||||
import { flushPromises } from '@vue/test-utils';
|
import { flushPromises } from '@vue/test-utils';
|
||||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
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) => {
|
vi.mock('../../services/fetchLegalContent', async (importOriginal) => {
|
||||||
const actual = await importOriginal();
|
const actual = await importOriginal();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
getLegalNotice
|
getLegalNotice,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => getLegalNotice.mockRestore());
|
afterEach(getLegalNotice.mockRestore);
|
||||||
|
|
||||||
describe('LegalNoticeView', () => {
|
describe('LegalNoticeView', () => {
|
||||||
test('shows loading indicator', () => {
|
test('shows loading indicator', () => {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
|
|
||||||
import logo from '@/assets/images/logo.svg';
|
|
||||||
import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
||||||
|
import logo from '@/assets/images/logo.svg';
|
||||||
|
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -33,7 +33,9 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="navbar-search-bar-wrapper-small-screens-wrapper">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
|||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-nav .right-links a:hover{
|
.global-nav .right-links a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,29 +15,35 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Searchbar from '@/features/search/components/Searchbar.vue';
|
import SearchBar from '@/features/search/components/SearchBar.vue';
|
||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const searchQueryModel = defineModel();
|
const searchQueryModel = defineModel();
|
||||||
|
|
||||||
watch(() => route.name, name => {
|
watch(
|
||||||
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
|
() => route.name,
|
||||||
});
|
(name) => {
|
||||||
|
searchQueryModel.value =
|
||||||
|
name === 'searchResults' ? route.query.q || '' : '';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
watch(() => route.query.q, q => {
|
watch(
|
||||||
if (route.name === 'searchResults') {
|
() => route.query.q,
|
||||||
searchQueryModel.value = q || '';
|
(q) => {
|
||||||
}
|
if (route.name === 'searchResults') {
|
||||||
});
|
searchQueryModel.value = q || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Searchbar
|
<SearchBar
|
||||||
v-if="route.name === 'searchResults'"
|
v-if="route.name === 'searchResults'"
|
||||||
v-model="searchQueryModel"
|
v-model="searchQueryModel"
|
||||||
auto-submit
|
auto-submit
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,26 +15,25 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<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();
|
const searchQuery = defineModel();
|
||||||
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const props = defineProps(['autoSubmit'])
|
const props = defineProps(['autoSubmit']);
|
||||||
|
|
||||||
const submitSearch = function () {
|
const submitSearch = function submitSearch() {
|
||||||
if (props.autoSubmit !== undefined) {
|
if (props.autoSubmit !== undefined) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'searchResults',
|
name: 'searchResults',
|
||||||
query: { q: searchQuery.value }
|
query: { q: searchQuery.value },
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -42,10 +41,10 @@ const submitSearch = function () {
|
|||||||
<form @submit.prevent="submitSearch" class="search-form">
|
<form @submit.prevent="submitSearch" class="search-form">
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
<input
|
<input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="search"
|
type="search"
|
||||||
:placeholder="t('search.searchBar.placeholder')"
|
:placeholder="t('search.searchBar.placeholder')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -53,7 +52,11 @@ const submitSearch = function () {
|
|||||||
:title="t('search.searchBar.submit')"
|
:title="t('search.searchBar.submit')"
|
||||||
:aria-label="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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -70,7 +73,10 @@ const submitSearch = function () {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1.5px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
box-shadow: 0 0px 32px var(--blue-box-shadow);
|
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: var(--padding);
|
||||||
padding-left: var(--padding-left);
|
padding-left: var(--padding-left);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -91,7 +97,9 @@ const submitSearch = function () {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
||||||
width: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
width: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
||||||
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y));
|
border-radius: calc(
|
||||||
|
var(--content-height) * 0.5 + var(--submit-button-padding-y)
|
||||||
|
);
|
||||||
border: none;
|
border: none;
|
||||||
padding: var(--submit-button-padding-y) 20px;
|
padding: var(--submit-button-padding-y) 20px;
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
@@ -114,4 +122,4 @@ const submitSearch = function () {
|
|||||||
.search-form {
|
.search-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -28,8 +28,7 @@ const { t } = useI18n();
|
|||||||
<p>{{ t('error.tryAgainToAnotherTime') }}</p>
|
<p>{{ t('error.tryAgainToAnotherTime') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-results-container">
|
<div class="search-results-container"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -94,4 +93,4 @@ const { t } = useI18n();
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,33 +15,32 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<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 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 { t } = useI18n();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
|
|
||||||
const submitSearch = function () {
|
|
||||||
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="search-content main-content-padding">
|
<div class="search-content main-content-padding">
|
||||||
<header class="global-header">
|
<header class="global-header">
|
||||||
<img :src="logo" alt="Seekra" class="header-logo" />
|
<img :src="logo" alt="Seekra" class="header-logo" />
|
||||||
<span class="slogan">
|
<span class="slogan">
|
||||||
{{ t('slogan') }}
|
{{ t('slogan') }}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="search-container">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -83,4 +82,4 @@ const submitSearch = function () {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -16,4 +16,4 @@
|
|||||||
|
|
||||||
.settings-button:hover {
|
.settings-button:hover {
|
||||||
background-color: var(--light-hover);
|
background-color: var(--light-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Icon from '@/features/icons/components/Icon.vue';
|
|
||||||
|
|
||||||
import { useSettingsStore } from '../stores/settingsStore';
|
import { useSettingsStore } from '../stores/settingsStore';
|
||||||
|
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||||
import { computed, ref, useId, watch } from 'vue';
|
import { computed, ref, useId, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
@@ -26,11 +24,11 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
setting: {
|
setting: {
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
@@ -43,29 +41,29 @@ const doneButtonInitial = ref(true);
|
|||||||
const inputId = useId();
|
const inputId = useId();
|
||||||
const inputLabelId = useId();
|
const inputLabelId = useId();
|
||||||
|
|
||||||
const inputType = computed(() => ({
|
const inputType = computed(
|
||||||
number: 'number',
|
() =>
|
||||||
string: 'text'
|
({
|
||||||
}[props.setting.type]
|
number: 'number',
|
||||||
));
|
string: 'text',
|
||||||
|
})[props.setting.type],
|
||||||
const hasChanged = computed(() =>
|
|
||||||
inputModel.value !== store.get(props.path)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasChanged = computed(() => inputModel.value !== store.get(props.path));
|
||||||
|
|
||||||
watch(store, (newValue) => {
|
watch(store, (newValue) => {
|
||||||
const newInputValue = newValue.get(props.path);
|
const newInputValue = newValue.get(props.path);
|
||||||
if (inputModel.value === oldInputValue) {
|
if (inputModel.value === oldInputValue) {
|
||||||
inputModel.value = newInputValue;
|
inputModel.value = newInputValue;
|
||||||
oldInputValue = newInputValue;
|
oldInputValue = newInputValue;
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(inputModel, () => {
|
watch(inputModel, () => {
|
||||||
doneButtonInitial.value = false;
|
doneButtonInitial.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const apply = function apply () {
|
const apply = function apply() {
|
||||||
store.set(props.path, inputModel.value);
|
store.set(props.path, inputModel.value);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -95,7 +93,7 @@ const apply = function apply () {
|
|||||||
:title="t('settings.input.done.title')"
|
:title="t('settings.input.done.title')"
|
||||||
:aria-label="t('settings.input.done.ariaLabel')"
|
:aria-label="t('settings.input.done.ariaLabel')"
|
||||||
>
|
>
|
||||||
<Icon name="check-mark" />
|
<UIIcon name="check-mark" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +129,10 @@ const apply = function apply () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-container.done-button-shown .input {
|
.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 {
|
.done-button {
|
||||||
@@ -200,7 +201,7 @@ const apply = function apply () {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes done-button-disappear {
|
@keyframes done-button-disappear {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -212,4 +213,4 @@ const apply = function apply () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,22 +15,21 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Switch from './Switch.vue';
|
import SettingsSelection from './SettingsSelection.vue';
|
||||||
import SettingsInput from './SettingsInput.vue';
|
import SettingsInput from './SettingsInput.vue';
|
||||||
import Selection from './Selection.vue';
|
|
||||||
import SettingsPageLink from './SettingsPageLink.vue';
|
import SettingsPageLink from './SettingsPageLink.vue';
|
||||||
|
import SettingsSwitch from './SettingsSwitch.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
setting: {
|
setting: {
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ const props = defineProps({
|
|||||||
:key="setting"
|
:key="setting"
|
||||||
class="setting"
|
class="setting"
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingsSwitch
|
||||||
v-if="setting.type === 'bool'"
|
v-if="setting.type === 'bool'"
|
||||||
:setting="setting"
|
:setting="setting"
|
||||||
:path="`${props.path}.${setting.name}`"
|
:path="`${props.path}.${setting.name}`"
|
||||||
@@ -54,7 +53,7 @@ const props = defineProps({
|
|||||||
:setting="setting"
|
:setting="setting"
|
||||||
:path="`${props.path}.${setting.name}`"
|
:path="`${props.path}.${setting.name}`"
|
||||||
/>
|
/>
|
||||||
<Selection
|
<SettingsSelection
|
||||||
v-if="setting.type === 'selection'"
|
v-if="setting.type === 'selection'"
|
||||||
:setting="setting"
|
:setting="setting"
|
||||||
:path="`${props.path}.${setting.name}`"
|
:path="`${props.path}.${setting.name}`"
|
||||||
@@ -79,15 +78,15 @@ const props = defineProps({
|
|||||||
margin-bottom: calc(var(--size) - 0.2em);
|
margin-bottom: calc(var(--size) - 0.2em);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading[aria-level="2"] {
|
.section-heading[aria-level='2'] {
|
||||||
--size: 1.8em;
|
--size: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading[aria-level="3"] {
|
.section-heading[aria-level='3'] {
|
||||||
--size: 1.6em;
|
--size: 1.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading[aria-level="4"] {
|
.section-heading[aria-level='4'] {
|
||||||
--size: 1.4em;
|
--size: 1.4em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Icon from '@/features/icons/components/Icon.vue';
|
|
||||||
import { useSettingsPage } from '../composables/useSettingsPage';
|
import { useSettingsPage } from '../composables/useSettingsPage';
|
||||||
|
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -25,15 +25,15 @@ const { goToSettingsPage } = useSettingsPage();
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
target: {
|
target: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
displayName: {
|
displayName: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: String,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const followLink = function followLink () {
|
const followLink = function followLink() {
|
||||||
goToSettingsPage(props.target);
|
goToSettingsPage(props.target);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -42,11 +42,13 @@ const followLink = function followLink () {
|
|||||||
<button
|
<button
|
||||||
class="settings-button button settings-page-link"
|
class="settings-button button settings-page-link"
|
||||||
:title="props.displayName"
|
: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"
|
@click="followLink"
|
||||||
>
|
>
|
||||||
{{ props.displayName }}
|
{{ props.displayName }}
|
||||||
<Icon name="chevron-right" size="1em" />
|
<UIIcon name="chevron-right" size="1em" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -58,4 +60,4 @@ const followLink = function followLink () {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Icon from '@/features/icons/components/Icon.vue';
|
|
||||||
import { useSettingsStore } from '../stores/settingsStore';
|
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';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const normalizeSelectedValue = function normalizeSelectedValue (value) {
|
const normalizeSelectedValue = function normalizeSelectedValue(value) {
|
||||||
if (props.setting.allowMultiple) {
|
if (props.setting.allowMultiple) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value;
|
return value;
|
||||||
@@ -44,19 +44,22 @@ const normalizeSelectedValue = function normalizeSelectedValue (value) {
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
setting: {
|
setting: {
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
|
|
||||||
const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio');
|
const optionType = computed(() =>
|
||||||
|
props.setting.allowMultiple ? 'checkbox' : 'radio',
|
||||||
|
);
|
||||||
const selected = computed({
|
const selected = computed({
|
||||||
get: () => normalizeSelectedValue(store.get(props.path) ?? props.setting.default),
|
get: () =>
|
||||||
set: (value) => store.set(props.path, value)
|
normalizeSelectedValue(store.get(props.path) ?? props.setting.default),
|
||||||
|
set: (value) => store.set(props.path, value),
|
||||||
});
|
});
|
||||||
|
|
||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
@@ -72,7 +75,11 @@ const labelId = useId();
|
|||||||
<label
|
<label
|
||||||
class="settings-button"
|
class="settings-button"
|
||||||
:title="t(option.i18n)"
|
: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
|
<input
|
||||||
:type="optionType"
|
:type="optionType"
|
||||||
@@ -80,9 +87,13 @@ const labelId = useId();
|
|||||||
:value="option.name"
|
:value="option.name"
|
||||||
class="option-input"
|
class="option-input"
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
>
|
/>
|
||||||
<div class="option-check-mark-wrapper">
|
<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>
|
||||||
<div class="input-label">
|
<div class="input-label">
|
||||||
{{ t(option.i18n) }}
|
{{ t(option.i18n) }}
|
||||||
@@ -122,4 +133,4 @@ const labelId = useId();
|
|||||||
.option-input:checked + .option-check-mark-wrapper .option-check-mark {
|
.option-input:checked + .option-check-mark-wrapper .option-check-mark {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -23,11 +23,11 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
setting: {
|
setting: {
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
@@ -42,7 +42,7 @@ watch(store, (newStore) => {
|
|||||||
// set value after registering watcher to avoid immediate value change
|
// set value after registering watcher to avoid immediate value change
|
||||||
enabled.value = store.get(props.path) ?? props.setting.default;
|
enabled.value = store.get(props.path) ?? props.setting.default;
|
||||||
|
|
||||||
const toggle = function toggle () {
|
const toggle = function toggle() {
|
||||||
enabled.value = !enabled.value;
|
enabled.value = !enabled.value;
|
||||||
store.set(props.path, enabled.value);
|
store.set(props.path, enabled.value);
|
||||||
};
|
};
|
||||||
@@ -106,4 +106,4 @@ const toggle = function toggle () {
|
|||||||
.switch-wrapper.enabled .switch-point {
|
.switch-wrapper.enabled .switch-point {
|
||||||
margin-left: var(--point-size);
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||||
import SettingsInput from '../SettingsInput.vue';
|
import SettingsInput from '../SettingsInput.vue';
|
||||||
import { mountComponent } from '@/test-utils/mountComponent';
|
import { mountComponent } from '@/test-utils/mountComponent';
|
||||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
const getWrapper = function getWrapper({ type = 'string', i18n = 'input', defaultValue = undefined, piniaOptions = {} } = {}) {
|
const getWrapper = function getWrapper({
|
||||||
return mountComponent(SettingsInput, {
|
type = 'string',
|
||||||
attrs: {
|
i18n = 'input',
|
||||||
setting: {
|
defaultValue = undefined,
|
||||||
name: 'input',
|
piniaOptions = {},
|
||||||
default: defaultValue,
|
} = {}) {
|
||||||
i18n,
|
return mountComponent(
|
||||||
type
|
SettingsInput,
|
||||||
|
{
|
||||||
|
attrs: {
|
||||||
|
setting: {
|
||||||
|
name: 'input',
|
||||||
|
default: defaultValue,
|
||||||
|
i18n,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
path: 'input',
|
||||||
},
|
},
|
||||||
path: 'input'
|
},
|
||||||
}
|
{},
|
||||||
}, {}, [], piniaOptions);
|
[],
|
||||||
}
|
piniaOptions,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
describe('SettingsInput', () => {
|
describe('SettingsInput', () => {
|
||||||
test('uses correct input type', () => {
|
test('uses correct input type', () => {
|
||||||
@@ -63,8 +74,8 @@ describe('SettingsInput', () => {
|
|||||||
setupStores: () => {
|
setupStores: () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.settings.input = value;
|
store.settings.input = value;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const inputElement = stringSettingWrapper.find('input');
|
const inputElement = stringSettingWrapper.find('input');
|
||||||
|
|
||||||
@@ -76,7 +87,7 @@ describe('SettingsInput', () => {
|
|||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
i18n: 'input',
|
i18n: 'input',
|
||||||
defaultValue: value
|
defaultValue: value,
|
||||||
});
|
});
|
||||||
const inputElement = wrapper.find('input');
|
const inputElement = wrapper.find('input');
|
||||||
|
|
||||||
@@ -89,13 +100,13 @@ describe('SettingsInput', () => {
|
|||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
i18n: 'input',
|
i18n: 'input',
|
||||||
defaultValue: defaultValue,
|
defaultValue,
|
||||||
piniaOptions: {
|
piniaOptions: {
|
||||||
setupStores: () => {
|
setupStores: () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.settings.input = storeValue;
|
store.settings.input = storeValue;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const inputElement = wrapper.find('input');
|
const inputElement = wrapper.find('input');
|
||||||
|
|
||||||
@@ -173,15 +184,15 @@ describe('SettingsInput', () => {
|
|||||||
|
|
||||||
await form.trigger('submit');
|
await form.trigger('submit');
|
||||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||||
|
|
||||||
await inputElement.setValue('a');
|
await inputElement.setValue('a');
|
||||||
expect(form.classes('done-button-shown')).toBeTruthy();
|
expect(form.classes('done-button-shown')).toBeTruthy();
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.set('input', 'a');
|
store.set('input', 'a');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||||
|
|
||||||
await inputElement.setValue('');
|
await inputElement.setValue('');
|
||||||
store.set('input', '');
|
store.set('input', '');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -191,20 +202,24 @@ describe('SettingsInput', () => {
|
|||||||
test('shows correct translation', () => {
|
test('shows correct translation', () => {
|
||||||
const translation = 'Text input';
|
const translation = 'Text input';
|
||||||
|
|
||||||
const wrapper = mountComponent(SettingsInput, {
|
const wrapper = mountComponent(
|
||||||
attrs: {
|
SettingsInput,
|
||||||
setting: {
|
{
|
||||||
name: 'input',
|
attrs: {
|
||||||
i18n: 'input',
|
setting: {
|
||||||
type: 'string',
|
name: 'input',
|
||||||
|
i18n: 'input',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
path: 'input',
|
||||||
},
|
},
|
||||||
path: 'input'
|
},
|
||||||
}
|
{
|
||||||
}, {
|
input: translation,
|
||||||
input: translation
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const label = wrapper.find('label');
|
const label = wrapper.find('label');
|
||||||
expect(label.text()).toBe(translation);
|
expect(label.text()).toBe(translation);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,44 +14,53 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Selection from '../Selection.vue';
|
import SettingsSelection from '../SettingsSelection.vue';
|
||||||
import SettingsInput from '../SettingsInput.vue';
|
import SettingsInput from '../SettingsInput.vue';
|
||||||
import SettingsPage from '../SettingsPage.vue';
|
import SettingsPage from '../SettingsPage.vue';
|
||||||
import SettingsPageLink from '../SettingsPageLink.vue';
|
import SettingsPageLink from '../SettingsPageLink.vue';
|
||||||
import Switch from '../Switch.vue';
|
import SettingsSwitch from '../SettingsSwitch.vue';
|
||||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
const getWrapper = function getWrapper({
|
||||||
const getWrapper = function getWrapper({ content = [], translations = {}, piniaOptions = {} }) {
|
content = [],
|
||||||
return mountComponent(SettingsPage, {
|
translations = {},
|
||||||
attrs: {
|
piniaOptions = {},
|
||||||
setting: {
|
}) {
|
||||||
name: 'section',
|
return mountComponent(
|
||||||
type: 'section',
|
SettingsPage,
|
||||||
i18n: 'section',
|
{
|
||||||
content: content
|
attrs: {
|
||||||
|
setting: {
|
||||||
|
name: 'section',
|
||||||
|
type: 'section',
|
||||||
|
i18n: 'section',
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
path: 'section',
|
||||||
},
|
},
|
||||||
path: 'section'
|
},
|
||||||
}
|
translations,
|
||||||
}, translations, [], piniaOptions);
|
[],
|
||||||
|
piniaOptions,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const exampleSettings = [
|
const exampleSettings = [
|
||||||
{
|
{
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
name: 'b0',
|
name: 'b0',
|
||||||
i18n: 'settings.section.b0'
|
i18n: 'settings.section.b0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
name: 'n0',
|
name: 'n0',
|
||||||
i18n: 'settings.section.n0'
|
i18n: 'settings.section.n0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'st0',
|
name: 'st0',
|
||||||
i18n: 'settings.section1.st0'
|
i18n: 'settings.section1.st0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'selection',
|
type: 'selection',
|
||||||
@@ -61,14 +70,14 @@ const exampleSettings = [
|
|||||||
{ name: 'o0', i18n: 'settings.section.se0.o0' },
|
{ name: 'o0', i18n: 'settings.section.se0.o0' },
|
||||||
{ name: 'o1', i18n: 'settings.section.se0.o1' },
|
{ name: 'o1', i18n: 'settings.section.se0.o1' },
|
||||||
{ name: 'o2', i18n: 'settings.section.se0.o2' },
|
{ name: 'o2', i18n: 'settings.section.se0.o2' },
|
||||||
{ name: 'o3', i18n: 'settings.section.se0.o3' }
|
{ name: 'o3', i18n: 'settings.section.se0.o3' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
name: 's0',
|
name: 's0',
|
||||||
i18n: 'settings.section.s0',
|
i18n: 'settings.section.s0',
|
||||||
content: []
|
content: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
@@ -79,17 +88,17 @@ const exampleSettings = [
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
name: 'b0',
|
name: 'b0',
|
||||||
i18n: 'settings.section.s1.b0',
|
i18n: 'settings.section.s1.b0',
|
||||||
default: true
|
default: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('SettingsPage', () => {
|
describe('SettingsPage', () => {
|
||||||
test('shows correct heading', () => {
|
test('shows correct heading', () => {
|
||||||
const translation = 'Example section';
|
const translation = 'Example section';
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
translations: { section: translation }
|
translations: { section: translation },
|
||||||
});
|
});
|
||||||
|
|
||||||
const heading = wrapper.find('.section-heading');
|
const heading = wrapper.find('.section-heading');
|
||||||
@@ -98,20 +107,21 @@ describe('SettingsPage', () => {
|
|||||||
|
|
||||||
test('shows settings correctly', () => {
|
test('shows settings correctly', () => {
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
content: exampleSettings
|
content: exampleSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const settingsWrappers = wrapper.findAll('.setting');
|
const settingsWrappers = wrapper.findAll('.setting');
|
||||||
expect(settingsWrappers.length).toBe(exampleSettings.length);
|
expect(settingsWrappers.length).toBe(exampleSettings.length);
|
||||||
|
|
||||||
exampleSettings.forEach((expectedSetting, index) => {
|
exampleSettings.forEach((expectedSetting, index) => {
|
||||||
const settingWrapper = settingsWrappers[index];
|
const settingWrapper = settingsWrappers[index];
|
||||||
|
|
||||||
const switchSetting = settingWrapper.findComponent(Switch);
|
const switchSetting = settingWrapper.findComponent(SettingsSwitch);
|
||||||
const inputSetting = settingWrapper.findComponent(SettingsInput);
|
const inputSetting = settingWrapper.findComponent(SettingsInput);
|
||||||
const selectionSetting = settingWrapper.findComponent(Selection);
|
const selectionSetting =
|
||||||
const settingsPageLink = settingWrapper.findComponent(SettingsPageLink);
|
settingWrapper.findComponent(SettingsSelection);
|
||||||
|
const settingsPageLink =
|
||||||
|
settingWrapper.findComponent(SettingsPageLink);
|
||||||
|
|
||||||
let setting;
|
let setting;
|
||||||
if (expectedSetting.type === 'bool') {
|
if (expectedSetting.type === 'bool') {
|
||||||
@@ -121,7 +131,10 @@ describe('SettingsPage', () => {
|
|||||||
expect(settingsPageLink.exists()).toBe(false);
|
expect(settingsPageLink.exists()).toBe(false);
|
||||||
|
|
||||||
setting = switchSetting;
|
setting = switchSetting;
|
||||||
} else if (expectedSetting.type === 'string' || expectedSetting.type === 'number') {
|
} else if (
|
||||||
|
expectedSetting.type === 'string' ||
|
||||||
|
expectedSetting.type === 'number'
|
||||||
|
) {
|
||||||
expect(switchSetting.exists()).toBe(false);
|
expect(switchSetting.exists()).toBe(false);
|
||||||
expect(inputSetting.exists()).toBe(true);
|
expect(inputSetting.exists()).toBe(true);
|
||||||
expect(selectionSetting.exists()).toBe(false);
|
expect(selectionSetting.exists()).toBe(false);
|
||||||
@@ -150,11 +163,15 @@ describe('SettingsPage', () => {
|
|||||||
expectedSetting.type === 'selection'
|
expectedSetting.type === 'selection'
|
||||||
) {
|
) {
|
||||||
expect(setting.props('setting')).toMatchObject(expectedSetting);
|
expect(setting.props('setting')).toMatchObject(expectedSetting);
|
||||||
expect(setting.props('path')).toBe(`section.${expectedSetting.name}`);
|
expect(setting.props('path')).toBe(
|
||||||
|
`section.${expectedSetting.name}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
expect(setting.props('displayName')).toBe(expectedSetting.i18n);
|
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();
|
const goToSettingsPage = vi.fn();
|
||||||
vi.mock('../../composables/useSettingsPage.js', () => ({
|
vi.mock('../../composables/useSettingsPage.js', () => ({
|
||||||
useSettingsPage: () => ({
|
useSettingsPage: () => ({
|
||||||
goToSettingsPage
|
goToSettingsPage,
|
||||||
})
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const getWrapper = function getWrapper({ target, displayName }) {
|
const getWrapper = function getWrapper({ target, displayName }) {
|
||||||
return mount(SettingsPageLink, {
|
return mount(SettingsPageLink, {
|
||||||
attrs: {
|
attrs: {
|
||||||
target,
|
target,
|
||||||
displayName
|
displayName,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
plugins: [
|
plugins: [createI18n()],
|
||||||
createI18n()
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,4 +54,4 @@ describe('SettingsPageLink', () => {
|
|||||||
|
|
||||||
expect(goToSettingsPage).toHaveBeenCalledWith(target);
|
expect(goToSettingsPage).toHaveBeenCalledWith(target);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,46 +14,52 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Selection from '../Selection.vue';
|
|
||||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
|
||||||
import { useSettingsStore } from '../../stores/settingsStore.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 { expect, describe, test } from 'vitest';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
const getWrapper = function getWrapper ({
|
const getWrapper = function getWrapper({
|
||||||
i18n = 'selection',
|
i18n = 'selection',
|
||||||
options = [],
|
options = [],
|
||||||
allowMultiple = false,
|
allowMultiple = false,
|
||||||
defaultValue = undefined,
|
defaultValue = undefined,
|
||||||
translations = {},
|
translations = {},
|
||||||
piniaOptions = {}
|
piniaOptions = {},
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return mountComponent(Selection, {
|
return mountComponent(
|
||||||
attrs: {
|
SettingsSelection,
|
||||||
setting: {
|
{
|
||||||
name: 'selection',
|
attrs: {
|
||||||
type: 'selection',
|
setting: {
|
||||||
i18n,
|
name: 'selection',
|
||||||
options,
|
type: 'selection',
|
||||||
allowMultiple,
|
i18n,
|
||||||
default: defaultValue
|
options,
|
||||||
|
allowMultiple,
|
||||||
|
default: defaultValue,
|
||||||
|
},
|
||||||
|
path: 'selection',
|
||||||
},
|
},
|
||||||
path: 'selection'
|
},
|
||||||
}
|
translations,
|
||||||
}, translations, [], piniaOptions);
|
[],
|
||||||
|
piniaOptions,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChecked = function getChecked (inputs) {
|
const getChecked = function getChecked(inputs) {
|
||||||
return inputs.map((input) => input.element.matches(':checked'));
|
return inputs.map((input) => input.element.matches(':checked'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const exampleOptions = [
|
const exampleOptions = [
|
||||||
{ name: 'o0', i18n: 'o0' },
|
{ name: 'o0', i18n: 'o0' },
|
||||||
{ name: 'o1', i18n: 'o1' },
|
{ name: 'o1', i18n: 'o1' },
|
||||||
{ name: 'o2', i18n: 'o2' }
|
{ name: 'o2', i18n: 'o2' },
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('Selection', () => {
|
describe('SettingsSelection', () => {
|
||||||
test('displays options correctly', () => {
|
test('displays options correctly', () => {
|
||||||
const wrapper = getWrapper({ options: exampleOptions });
|
const wrapper = getWrapper({ options: exampleOptions });
|
||||||
|
|
||||||
@@ -71,7 +77,7 @@ describe('Selection', () => {
|
|||||||
expect(input.attributes('name')).toBe('selection');
|
expect(input.attributes('name')).toBe('selection');
|
||||||
|
|
||||||
expect(input.element.matches(':checked')).toBe(false);
|
expect(input.element.matches(':checked')).toBe(false);
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows value from store', () => {
|
test('shows value from store', () => {
|
||||||
@@ -81,8 +87,8 @@ describe('Selection', () => {
|
|||||||
setupStores: () => {
|
setupStores: () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.set('selection', 'o1');
|
store.set('selection', 'o1');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputs = wrapper.findAll('input');
|
const inputs = wrapper.findAll('input');
|
||||||
@@ -97,8 +103,8 @@ describe('Selection', () => {
|
|||||||
setupStores: () => {
|
setupStores: () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.set('selection', 'o1');
|
store.set('selection', 'o1');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
@@ -123,9 +129,9 @@ describe('Selection', () => {
|
|||||||
setupStores: () => {
|
setupStores: () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.set('selection', ['o1', 'o2']);
|
store.set('selection', ['o1', 'o2']);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
allowMultiple: true
|
allowMultiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputs = wrapper.findAll('input');
|
const inputs = wrapper.findAll('input');
|
||||||
@@ -136,7 +142,7 @@ describe('Selection', () => {
|
|||||||
test('shows default value', () => {
|
test('shows default value', () => {
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
options: exampleOptions,
|
options: exampleOptions,
|
||||||
defaultValue: 'o2'
|
defaultValue: 'o2',
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputs = wrapper.findAll('input');
|
const inputs = wrapper.findAll('input');
|
||||||
@@ -148,7 +154,7 @@ describe('Selection', () => {
|
|||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
options: exampleOptions,
|
options: exampleOptions,
|
||||||
defaultValue: ['o0', 'o1'],
|
defaultValue: ['o0', 'o1'],
|
||||||
allowMultiple: true
|
allowMultiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputs = wrapper.findAll('input');
|
const inputs = wrapper.findAll('input');
|
||||||
@@ -164,8 +170,8 @@ describe('Selection', () => {
|
|||||||
setupStores: () => {
|
setupStores: () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.set('selection', 'o1');
|
store.set('selection', 'o1');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputs = wrapper.findAll('input');
|
const inputs = wrapper.findAll('input');
|
||||||
@@ -199,33 +205,36 @@ describe('Selection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('stores multiple values', async () => {
|
test('stores multiple values', async () => {
|
||||||
const setValue = async function setValue (input, value) {
|
const setValue = async function setValue(input, value) {
|
||||||
input.setValue(value);
|
input.setValue(value);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
};
|
};
|
||||||
|
|
||||||
const check = function check (input) {
|
const check = function check(input) {
|
||||||
return setValue(input, true);
|
return setValue(input, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uncheck = function uncheck (input) {
|
const uncheck = function uncheck(input) {
|
||||||
return setValue(input, false);
|
return setValue(input, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectSelectionValue = function expectSelectionValue (value, options = exampleOptions) {
|
const expectSelectionValue = function expectSelectionValue(
|
||||||
|
value,
|
||||||
|
options = exampleOptions,
|
||||||
|
) {
|
||||||
value.sort();
|
value.sort();
|
||||||
expect(store.get('selection').sort()).toStrictEqual(value);
|
expect(store.get('selection').sort()).toStrictEqual(value);
|
||||||
const checkedInputs = getChecked(inputs);
|
const checkedInputs = getChecked(inputs);
|
||||||
expect(
|
expect(
|
||||||
options
|
options
|
||||||
.map((option) => option.name)
|
.map((option) => option.name)
|
||||||
.filter((entry, i) => checkedInputs[i])
|
.filter((_, i) => checkedInputs[i]),
|
||||||
).toStrictEqual(value);
|
).toStrictEqual(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
options: exampleOptions,
|
options: exampleOptions,
|
||||||
allowMultiple: true
|
allowMultiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
@@ -237,7 +246,7 @@ describe('Selection', () => {
|
|||||||
const inputO2 = inputs[2];
|
const inputO2 = inputs[2];
|
||||||
|
|
||||||
await check(inputO0);
|
await check(inputO0);
|
||||||
expectSelectionValue(['o0'])
|
expectSelectionValue(['o0']);
|
||||||
|
|
||||||
await uncheck(inputO0);
|
await uncheck(inputO0);
|
||||||
expectSelectionValue([]);
|
expectSelectionValue([]);
|
||||||
@@ -283,11 +292,11 @@ describe('Selection', () => {
|
|||||||
const translation = 'Selection setting';
|
const translation = 'Selection setting';
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
translations: {
|
translations: {
|
||||||
selection: translation
|
selection: translation,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const label = wrapper.find('.label');
|
const label = wrapper.find('.label');
|
||||||
expect(label.text()).toBe(translation);
|
expect(label.text()).toBe(translation);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -14,32 +14,38 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Switch from '../Switch.vue';
|
|
||||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
|
||||||
import { useSettingsStore } from '../../stores/settingsStore.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';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
const getWrapper = function getWrapper ({
|
const getWrapper = function getWrapper({
|
||||||
i18n = 'switch1',
|
i18n = 'switch1',
|
||||||
defaultValue = undefined,
|
defaultValue = undefined,
|
||||||
translations = {},
|
translations = {},
|
||||||
piniaOptions = {}
|
piniaOptions = {},
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return mountComponent(Switch, {
|
return mountComponent(
|
||||||
attrs: {
|
SettingsSwitch,
|
||||||
setting: {
|
{
|
||||||
type: 'bool',
|
attrs: {
|
||||||
name: 'switch',
|
setting: {
|
||||||
i18n,
|
type: 'bool',
|
||||||
default: defaultValue
|
name: 'switch',
|
||||||
|
i18n,
|
||||||
|
default: defaultValue,
|
||||||
|
},
|
||||||
|
path: 'switch',
|
||||||
},
|
},
|
||||||
path: 'switch'
|
},
|
||||||
}
|
translations,
|
||||||
}, translations, [], piniaOptions);
|
[],
|
||||||
|
piniaOptions,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Switch', () => {
|
describe('SettingsSwitch', () => {
|
||||||
test('shows value from store', async () => {
|
test('shows value from store', async () => {
|
||||||
const wrapper = getWrapper();
|
const wrapper = getWrapper();
|
||||||
|
|
||||||
@@ -61,7 +67,7 @@ describe('Switch', () => {
|
|||||||
|
|
||||||
test('shows default value', () => {
|
test('shows default value', () => {
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
defaultValue: true
|
defaultValue: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const switchElement = wrapper.find('.switch-wrapper');
|
const switchElement = wrapper.find('.switch-wrapper');
|
||||||
@@ -75,8 +81,8 @@ describe('Switch', () => {
|
|||||||
setupStores: () => {
|
setupStores: () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
store.set('switch', false);
|
store.set('switch', false);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const switchElement = wrapper.find('.switch-wrapper');
|
const switchElement = wrapper.find('.switch-wrapper');
|
||||||
@@ -89,7 +95,7 @@ describe('Switch', () => {
|
|||||||
|
|
||||||
const switchElement = wrapper.find('.switch-wrapper');
|
const switchElement = wrapper.find('.switch-wrapper');
|
||||||
expect(switchElement.classes('enabled')).toBeFalsy();
|
expect(switchElement.classes('enabled')).toBeFalsy();
|
||||||
|
|
||||||
store.set('switch', true);
|
store.set('switch', true);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(switchElement.classes('enabled')).toBeTruthy();
|
expect(switchElement.classes('enabled')).toBeTruthy();
|
||||||
@@ -97,29 +103,29 @@ describe('Switch', () => {
|
|||||||
|
|
||||||
test('toggles value in store', async () => {
|
test('toggles value in store', async () => {
|
||||||
const wrapper = getWrapper();
|
const wrapper = getWrapper();
|
||||||
|
|
||||||
const switchElement = wrapper.find('.switch-wrapper');
|
const switchElement = wrapper.find('.switch-wrapper');
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
expect(store.get('switch')).toBeUndefined();
|
expect(store.get('switch')).toBeUndefined();
|
||||||
|
|
||||||
switchElement.trigger('click');
|
switchElement.trigger('click');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(store.get('switch')).toBe(true);
|
expect(store.get('switch')).toBe(true);
|
||||||
|
|
||||||
switchElement.trigger('click');
|
switchElement.trigger('click');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(store.get('switch')).toBe(false);
|
expect(store.get('switch')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows correct translation', () => {
|
test('shows correct translation', () => {
|
||||||
const translation = 'Switch';
|
const translation = 'Switch';
|
||||||
|
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
translations: { switch1: translation }
|
translations: { switch1: translation },
|
||||||
})
|
});
|
||||||
|
|
||||||
const label = wrapper.find('label');
|
const label = wrapper.find('label');
|
||||||
expect(label.text()).toBe(translation);
|
expect(label.text()).toBe(translation);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -33,17 +33,17 @@ vi.mock('../../config.js', () => ({
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
i18n: '',
|
i18n: '',
|
||||||
name: 'number',
|
name: 'number',
|
||||||
default: settingDefaultValue
|
default: settingDefaultValue,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
i18n: '',
|
i18n: '',
|
||||||
name: 'bool'
|
name: 'bool',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('useSettings', () => {
|
describe('useSettings', () => {
|
||||||
@@ -59,7 +59,7 @@ describe('useSettings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('returns default value', () => {
|
test('returns default value', () => {
|
||||||
const { getSetting, setSetting } = useSettings();
|
const { getSetting } = useSettings();
|
||||||
expect(getSetting(settingPath)).toBe(settingDefaultValue);
|
expect(getSetting(settingPath)).toBe(settingDefaultValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ beforeEach(() => {
|
|||||||
setActivePinia(
|
setActivePinia(
|
||||||
createTestingPinia({
|
createTestingPinia({
|
||||||
createSpy: vi.fn,
|
createSpy: vi.fn,
|
||||||
stubActions: false
|
stubActions: false,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import { describe, expect, test, vi } from 'vitest';
|
|||||||
const pushRoute = vi.fn();
|
const pushRoute = vi.fn();
|
||||||
vi.mock('vue-router', () => ({
|
vi.mock('vue-router', () => ({
|
||||||
useRouter: () => ({
|
useRouter: () => ({
|
||||||
push: pushRoute
|
push: pushRoute,
|
||||||
})
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('useSettingsPage', () => {
|
describe('useSettingsPage', () => {
|
||||||
@@ -32,9 +32,9 @@ describe('useSettingsPage', () => {
|
|||||||
expect(pushRoute).toHaveBeenCalledWith({
|
expect(pushRoute).toHaveBeenCalledWith({
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
params: {
|
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.
|
* Provides access to the stored settings.
|
||||||
*/
|
*/
|
||||||
export const useSettings = function useSettings () {
|
export const useSettings = function useSettings() {
|
||||||
/**
|
/**
|
||||||
* Returns the value of a specific setting.
|
* Returns the value of a specific setting.
|
||||||
* @param {string[]} key - The settings key.
|
* @param {string[]} key - The settings key.
|
||||||
* @return The value of the setting.
|
* @return The value of the setting.
|
||||||
*/
|
*/
|
||||||
const getSetting = function getSetting (key) {
|
const getSetting = function getSetting(key) {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
settingsStore.get(key.join('.')) ??
|
settingsStore.get(key.join('.')) ??
|
||||||
getSettingRecursively(
|
getSettingRecursively(key, settings.contents ?? [])?.default
|
||||||
key, settings.contents ?? []
|
|
||||||
)?.default
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,7 +41,7 @@ export const useSettings = function useSettings () {
|
|||||||
* @param {string[]} key - The setting key.
|
* @param {string[]} key - The setting key.
|
||||||
* @param value - The new value for the setting.
|
* @param value - The new value for the setting.
|
||||||
*/
|
*/
|
||||||
const setSetting = function setSetting (key, value) {
|
const setSetting = function setSetting(key, value) {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
settingsStore.set(key.join('.'), value);
|
settingsStore.set(key.join('.'), value);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ limitations under the License.
|
|||||||
import { getSettingsPagePathSegments } from '../utils/settingsPage';
|
import { getSettingsPagePathSegments } from '../utils/settingsPage';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
export const useSettingsPage = function useSettingsPage () {
|
export const useSettingsPage = function useSettingsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const goToSettingsPage = function goToSettingsPage (pagePath) {
|
const goToSettingsPage = function goToSettingsPage(pagePath) {
|
||||||
return router.push({
|
return router.push({
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
params: {
|
params: {
|
||||||
rest: getSettingsPagePathSegments(pagePath)
|
rest: getSettingsPagePathSegments(pagePath),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return { goToSettingsPage };
|
return { goToSettingsPage };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ export default {
|
|||||||
default: defaultLanguage,
|
default: defaultLanguage,
|
||||||
options: SUPPORTED_LANGUAGES.map((language) => ({
|
options: SUPPORTED_LANGUAGES.map((language) => ({
|
||||||
name: language,
|
name: language,
|
||||||
i18n: `preferences.locale.languages.${language}`
|
i18n: `preferences.locale.languages.${language}`,
|
||||||
}))
|
})),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'appearance',
|
name: 'appearance',
|
||||||
@@ -57,19 +57,19 @@ export default {
|
|||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'auto',
|
name: 'auto',
|
||||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.auto'
|
i18n: 'settings.settings.appearance.contents.colorScheme.options.auto',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'light',
|
name: 'light',
|
||||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.light'
|
i18n: 'settings.settings.appearance.contents.colorScheme.options.light',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dark',
|
name: 'dark',
|
||||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.dark'
|
i18n: 'settings.settings.appearance.contents.colorScheme.options.dark',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,26 +17,30 @@ limitations under the License.
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore(
|
||||||
const settings = reactive({});
|
'settings',
|
||||||
|
() => {
|
||||||
|
const settings = reactive({});
|
||||||
|
|
||||||
const set = function set (key, value) {
|
const set = function set(key, value) {
|
||||||
settings[key] = value;
|
settings[key] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const get = function get (key) {
|
const get = function get(key) {
|
||||||
return settings[key];
|
return settings[key];
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = function remove (key) {
|
const remove = function remove(key) {
|
||||||
delete settings[key];
|
delete settings[key];
|
||||||
};
|
};
|
||||||
|
|
||||||
const clear = function clear () {
|
const clear = function clear() {
|
||||||
Object.keys(settings).forEach(key => delete settings[key]);
|
Object.keys(settings).forEach((key) => delete settings[key]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { set, get, remove, clear, settings };
|
return { set, get, remove, clear, settings };
|
||||||
}, {
|
},
|
||||||
persist: true
|
{
|
||||||
});
|
persist: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -62,4 +62,4 @@ limitations under the License.
|
|||||||
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
|
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
|
||||||
* @typedef {{ contents: SettingConfigEntry[] }} FirstLevelSettingConfigEntry
|
* @typedef {{ contents: SettingConfigEntry[] }} FirstLevelSettingConfigEntry
|
||||||
* @typedef {{ contents: FirstLevelSettingConfigEntry[] }} SettingsConfig
|
* @typedef {{ contents: FirstLevelSettingConfigEntry[] }} SettingsConfig
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -47,17 +47,26 @@ describe('settingsPage', () => {
|
|||||||
{ path: 'a1.#b.c/', expected: ['a1', 'b', 'c'] },
|
{ 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-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....',
|
||||||
{ path: '..,...~..@a/#.+..)!&§[b.#§c..dä..)..', expected: ['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: 'a.@.b', expected: ['a', 'b'] },
|
||||||
{ path: 1, expected: [] },
|
{ path: 1, expected: [] },
|
||||||
{ path: false, expected: [] },
|
{ path: false, expected: [] },
|
||||||
{ path: true, expected: [] },
|
{ path: true, expected: [] },
|
||||||
{ path: null, expected: [] },
|
{ path: null, expected: [] },
|
||||||
{ path: undefined, expected: [] }
|
{ path: undefined, expected: [] },
|
||||||
])('returns $expected', async ({ path, expected }) => {
|
])('returns $expected', ({ path, expected }) => {
|
||||||
expect(getSettingsPagePathSegments(path)).toStrictEqual(expected);
|
expect(getSettingsPagePathSegments(path)).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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];
|
return settings.filter((setting) => setting.name === name)[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSettingRecursively = function getSettingRecursively (sectionPath, settings) {
|
export const getSettingRecursively = function getSettingRecursively(
|
||||||
const oneSettingsLevel = function oneSettingsLevel (cursor, acc) {
|
sectionPath,
|
||||||
|
settings,
|
||||||
|
) {
|
||||||
|
const oneSettingsLevel = function oneSettingsLevel(cursor, acc) {
|
||||||
const setting = getSetting(cursor[0], acc.content);
|
const setting = getSetting(cursor[0], acc.content);
|
||||||
if (!setting) return;
|
if (!setting) return;
|
||||||
|
|
||||||
@@ -28,15 +31,18 @@ export const getSettingRecursively = function getSettingRecursively (sectionPath
|
|||||||
const newCursor = cursor.slice(1, cursor.length);
|
const newCursor = cursor.slice(1, cursor.length);
|
||||||
|
|
||||||
if (newCursor.length !== 0) {
|
if (newCursor.length !== 0) {
|
||||||
return oneSettingsLevel(cursor.slice(1, cursor.length), setting);
|
return oneSettingsLevel(
|
||||||
|
cursor.slice(1, cursor.length),
|
||||||
|
setting,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return setting;
|
return setting;
|
||||||
};
|
}
|
||||||
} else if (cursor.length <= 1) {
|
} else if (cursor.length <= 1) {
|
||||||
return setting;
|
return setting;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
return oneSettingsLevel(sectionPath, { content: settings });
|
return oneSettingsLevel(sectionPath, { content: settings });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ limitations under the License.
|
|||||||
* @param {string} path - The settings page path.
|
* @param {string} path - The settings page path.
|
||||||
* @returns {Array} The settings page path segments.
|
* @returns {Array} The settings page path segments.
|
||||||
*/
|
*/
|
||||||
export const getSettingsPagePathSegments = function getSettingsPagePathSegments (path) {
|
export const getSettingsPagePathSegments = function getSettingsPagePathSegments(
|
||||||
|
path,
|
||||||
|
) {
|
||||||
if (typeof path !== 'string') return [];
|
if (typeof path !== 'string') return [];
|
||||||
return path
|
return path
|
||||||
.replace(/^\.+|\.+$/g, '')
|
.replace(/^\.+|\.+$/g, '')
|
||||||
.split('.')
|
.split('.')
|
||||||
.map(
|
.map((segment) => segment.replace(/[^a-zA-Z0-9-]/g, ''))
|
||||||
(segment) => segment.replace(/[^a-zA-Z0-9-]/g, '')
|
|
||||||
)
|
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,33 +16,38 @@ limitations under the License.
|
|||||||
|
|
||||||
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
|
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
|
||||||
|
|
||||||
export const assertString = function assertString (value, path) {
|
export const assertString = function assertString(value, path) {
|
||||||
if (typeof value !== 'string' || value.trim() === '') {
|
if (typeof value !== 'string' || value.trim() === '') {
|
||||||
throw new Error(`[settings] "${path}" must be a non-empty string`);
|
throw new Error(`[settings] "${path}" must be a non-empty string`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const assertType = function assertType (value, path) {
|
export const assertType = function assertType(value, path) {
|
||||||
if (!VALID_TYPES.includes(value)) {
|
if (!VALID_TYPES.includes(value)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`
|
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const validateSelectionOptions = function validateSelectionOptions (options, path) {
|
export const validateSelectionOptions = function validateSelectionOptions(
|
||||||
|
options,
|
||||||
|
path,
|
||||||
|
) {
|
||||||
if (!Array.isArray(options) || options.length === 0) {
|
if (!Array.isArray(options) || options.length === 0) {
|
||||||
throw new Error(`[settings] "${path}.options" must be a non-empty array`);
|
throw new Error(
|
||||||
};
|
`[settings] "${path}.options" must be a non-empty array`,
|
||||||
|
);
|
||||||
|
}
|
||||||
options.forEach((opt, i) => {
|
options.forEach((opt, i) => {
|
||||||
assertString(opt.name, `${path}.options[${i}].name`);
|
assertString(opt.name, `${path}.options[${i}].name`);
|
||||||
assertString(opt.i18n, `${path}.options[${i}].i18n`);
|
assertString(opt.i18n, `${path}.options[${i}].i18n`);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const validateEntry = function validateEntry (entry, path) {
|
export const validateEntry = function validateEntry(entry, path) {
|
||||||
assertType(entry.type, `${path}.type`);
|
assertType(entry.type, `${path}.type`);
|
||||||
|
|
||||||
assertString(entry.name, `${path}.name`);
|
assertString(entry.name, `${path}.name`);
|
||||||
assertString(entry.i18n, `${path}.i18n`);
|
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`);
|
throw new Error(`[settings] "${path}.content" must be an array`);
|
||||||
}
|
}
|
||||||
entry.content.forEach((child, i) =>
|
entry.content.forEach((child, i) =>
|
||||||
validateEntry(child, `${path}.content[${i}]`)
|
validateEntry(child, `${path}.content[${i}]`),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === 'selection') {
|
if (entry.type === 'selection') {
|
||||||
validateSelectionOptions(entry.options, path);
|
validateSelectionOptions(entry.options, path);
|
||||||
if (typeof entry.allowMultiple !== 'boolean' && entry.allowMultiple) {
|
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.default !== undefined) {
|
||||||
if (entry.type === 'bool' && typeof entry.default !== 'boolean') {
|
if (entry.type === 'bool' && typeof entry.default !== 'boolean') {
|
||||||
throw new Error(`[settings] "${path}.default" must be a boolean`);
|
throw new Error(`[settings] "${path}.default" must be a boolean`);
|
||||||
@@ -75,39 +82,56 @@ export const validateEntry = function validateEntry (entry, path) {
|
|||||||
if (entry.type === 'selection') {
|
if (entry.type === 'selection') {
|
||||||
if (entry.allowMultiple) {
|
if (entry.allowMultiple) {
|
||||||
if (!Array.isArray(entry.default)) {
|
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);
|
const allOptions = entry.options.map((option) => option.name);
|
||||||
entry.default.forEach((defaultValue, index) => {
|
entry.default.forEach((defaultValue, index) => {
|
||||||
if (typeof defaultValue !== 'string') {
|
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)) {
|
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 {
|
} else {
|
||||||
if (typeof entry.default !== 'string') {
|
if (typeof entry.default !== 'string') {
|
||||||
throw new Error(`[settings] "${path}.default" must be a string`);
|
throw new Error(
|
||||||
};
|
`[settings] "${path}.default" must be a string`,
|
||||||
if (!entry.options.map((option) => option.name).includes(entry.default)) {
|
);
|
||||||
throw new Error(`[settings] option "${path}.default" does not exist`);
|
}
|
||||||
};
|
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.name);
|
||||||
assertString(section.i18n);
|
assertString(section.i18n);
|
||||||
|
|
||||||
if (!Array.isArray(section.content)) {
|
if (!Array.isArray(section.content)) {
|
||||||
throw new Error(`[settings] "${path}.content" must be an array`);
|
throw new Error(`[settings] "${path}.content" must be an array`);
|
||||||
};
|
}
|
||||||
|
|
||||||
section.content.forEach((entry, i) =>
|
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
|
* @param {unknown} raw
|
||||||
* @returns {{ valid: true, config: import('../types/settingsConfig').SettingsConfig } | { valid: false, error: string }}
|
* @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') {
|
if (!raw || typeof raw !== 'object') {
|
||||||
throw new Error('[settings] Config must be an 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');
|
throw new Error('[settings] "contents" must be an array');
|
||||||
}
|
}
|
||||||
raw.contents.forEach((entry, i) =>
|
raw.contents.forEach((entry, i) =>
|
||||||
validateFirstLevelSection(entry, `contents[${i}]`)
|
validateFirstLevelSection(entry, `contents[${i}]`),
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
|
|
||||||
import SettingsPage from '../components/SettingsPage.vue';
|
import SettingsPage from '../components/SettingsPage.vue';
|
||||||
|
|
||||||
import settingsConfiguration from '../config.js';
|
import settingsConfiguration from '../config.js';
|
||||||
import { getSettingRecursively } from '../utils/getSetting.js';
|
import { getSettingRecursively } from '../utils/getSetting.js';
|
||||||
|
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -31,15 +30,13 @@ const router = useRouter();
|
|||||||
const settings = settingsConfiguration.contents;
|
const settings = settingsConfiguration.contents;
|
||||||
|
|
||||||
const activePath = computed(() => {
|
const activePath = computed(() => {
|
||||||
const segments = route.path
|
const segments = route.path.split('/').filter(Boolean);
|
||||||
.split('/')
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return segments.slice(1, segments.length);
|
return segments.slice(1, segments.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeSection = ref(null);
|
const activeSection = ref(null);
|
||||||
const updateSettings = async function updateSettings () {
|
const updateSettings = async function updateSettings() {
|
||||||
const setting = getSettingRecursively(activePath.value, settings);
|
const setting = getSettingRecursively(activePath.value, settings);
|
||||||
if (setting) {
|
if (setting) {
|
||||||
activeSection.value = setting;
|
activeSection.value = setting;
|
||||||
@@ -50,7 +47,9 @@ const updateSettings = async function updateSettings () {
|
|||||||
watch(route, (newRoute) => updateSettings(newRoute));
|
watch(route, (newRoute) => updateSettings(newRoute));
|
||||||
onMounted(updateSettings);
|
onMounted(updateSettings);
|
||||||
|
|
||||||
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSidebarFunction) {
|
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen(
|
||||||
|
toggleSidebarFunction,
|
||||||
|
) {
|
||||||
if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction();
|
if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -65,11 +64,15 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi
|
|||||||
<LeftSidebarLayout class="layout">
|
<LeftSidebarLayout class="layout">
|
||||||
<template #sidebar="{ toggleSidebar }">
|
<template #sidebar="{ toggleSidebar }">
|
||||||
<ul class="sidebar-sections-list">
|
<ul class="sidebar-sections-list">
|
||||||
<li v-for="section in settings">
|
<li v-for="section in settings" :key="section">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="`/settings/${section.name}`"
|
:to="`/settings/${section.name}`"
|
||||||
class="button button-link link sidebar-section"
|
class="button button-link link sidebar-section"
|
||||||
:class="{ active: activeSection && activeSection.name === section.name }"
|
:class="{
|
||||||
|
active:
|
||||||
|
activeSection &&
|
||||||
|
activeSection.name === section.name,
|
||||||
|
}"
|
||||||
@click="toggleSidebarIfSmallScreen(toggleSidebar)"
|
@click="toggleSidebarIfSmallScreen(toggleSidebar)"
|
||||||
>
|
>
|
||||||
{{ t(section.i18n) }}
|
{{ t(section.i18n) }}
|
||||||
@@ -137,4 +140,4 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
{
|
{
|
||||||
name: 's0',
|
name: 's0',
|
||||||
i18n: 'settings.s0',
|
i18n: 'settings.s0',
|
||||||
content: []
|
content: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's1',
|
name: 's1',
|
||||||
@@ -32,17 +32,17 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
{
|
{
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
name: 'b0',
|
name: 'b0',
|
||||||
i18n: 'settings.s1.b0'
|
i18n: 'settings.s1.b0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
name: 'n0',
|
name: 'n0',
|
||||||
i18n: 'settings.s1.n0'
|
i18n: 'settings.s1.n0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'st0',
|
name: 'st0',
|
||||||
i18n: 'settings.s1.st0'
|
i18n: 'settings.s1.st0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'selection',
|
type: 'selection',
|
||||||
@@ -52,14 +52,14 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
{ name: 'o0', i18n: 'settings.s1.se0.o0' },
|
{ name: 'o0', i18n: 'settings.s1.se0.o0' },
|
||||||
{ name: 'o1', i18n: 'settings.s1.se0.o1' },
|
{ name: 'o1', i18n: 'settings.s1.se0.o1' },
|
||||||
{ name: 'o2', i18n: 'settings.s1.se0.o2' },
|
{ name: 'o2', i18n: 'settings.s1.se0.o2' },
|
||||||
{ name: 'o3', i18n: 'settings.s1.se0.o3' }
|
{ name: 'o3', i18n: 'settings.s1.se0.o3' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
name: 's0',
|
name: 's0',
|
||||||
i18n: 'settings.s1.s0',
|
i18n: 'settings.s1.s0',
|
||||||
content: []
|
content: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
@@ -70,11 +70,11 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
name: 'b0',
|
name: 'b0',
|
||||||
i18n: 'settings.s1.s1.b0',
|
i18n: 'settings.s1.s1.b0',
|
||||||
default: true
|
default: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's2',
|
name: 's2',
|
||||||
@@ -89,11 +89,11 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
name: 'b0',
|
name: 'b0',
|
||||||
i18n: 'settings.s2.s0.b0',
|
i18n: 'settings.s2.s0.b0',
|
||||||
default: true
|
default: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's3',
|
name: 's3',
|
||||||
@@ -103,9 +103,9 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
type: 'section',
|
type: 'section',
|
||||||
name: 's0',
|
name: 's0',
|
||||||
i18n: 'settings.s3.s0',
|
i18n: 'settings.s3.s0',
|
||||||
content: []
|
content: [],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's4',
|
name: 's4',
|
||||||
@@ -119,10 +119,10 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
{ name: 'o0', i18n: 'settings.s4.se0.o0' },
|
{ name: 'o0', i18n: 'settings.s4.se0.o0' },
|
||||||
{ name: 'o1', i18n: 'settings.s4.se0.o1' },
|
{ name: 'o1', i18n: 'settings.s4.se0.o1' },
|
||||||
{ name: 'o2', i18n: 'settings.s4.se0.o2' },
|
{ name: 'o2', i18n: 'settings.s4.se0.o2' },
|
||||||
{ name: 'o3', i18n: 'settings.s4.se0.o3' }
|
{ name: 'o3', i18n: 'settings.s4.se0.o3' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's5',
|
name: 's5',
|
||||||
@@ -131,9 +131,9 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'st0',
|
name: 'st0',
|
||||||
i18n: 'settings.s5.st0'
|
i18n: 'settings.s5.st0',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's6',
|
name: 's6',
|
||||||
@@ -142,9 +142,9 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
name: 'n0',
|
name: 'n0',
|
||||||
i18n: 'settings.s6.n0'
|
i18n: 'settings.s6.n0',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's7',
|
name: 's7',
|
||||||
@@ -153,9 +153,9 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
{
|
{
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
name: 'b0',
|
name: 'b0',
|
||||||
i18n: 'settings.s7.b0'
|
i18n: 'settings.s7.b0',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's8',
|
name: 's8',
|
||||||
@@ -181,16 +181,16 @@ const settingsHoisted = vi.hoisted(() => [
|
|||||||
name: 'se0',
|
name: 'se0',
|
||||||
i18n: 'settings.s8.s0.s0.s0.se0',
|
i18n: 'settings.s8.s0.s0.s0.se0',
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
options: []
|
options: [],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
const settings = settingsHoisted.values();
|
const settings = settingsHoisted.values();
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ const originalCurrentSection = 's0';
|
|||||||
let currentSection = originalCurrentSection;
|
let currentSection = originalCurrentSection;
|
||||||
|
|
||||||
vi.mock('../../config.js', () => ({
|
vi.mock('../../config.js', () => ({
|
||||||
default: { contents: settingsHoisted }
|
default: { contents: settingsHoisted },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('vue-router', async (importOriginal) => {
|
vi.mock('vue-router', async (importOriginal) => {
|
||||||
@@ -206,12 +206,12 @@ vi.mock('vue-router', async (importOriginal) => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useRoute: () => ({
|
useRoute: () => ({
|
||||||
path: `/settings/${currentSection}`
|
path: `/settings/${currentSection}`,
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const getWrapper = function getWrapper ({ translations = {} } = {}) {
|
const getWrapper = function getWrapper({ translations = {} } = {}) {
|
||||||
return mountComponent(SettingsView, {}, translations, [], {});
|
return mountComponent(SettingsView, {}, translations, [], {});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -221,8 +221,8 @@ describe('SettingsView', () => {
|
|||||||
const translation = 'Settings';
|
const translation = 'Settings';
|
||||||
const wrapper = getWrapper({
|
const wrapper = getWrapper({
|
||||||
translations: {
|
translations: {
|
||||||
'preferences.settings': translation
|
'preferences.settings': translation,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const header = wrapper.find('header');
|
const header = wrapper.find('header');
|
||||||
@@ -242,8 +242,12 @@ describe('SettingsView', () => {
|
|||||||
expect(sidebarItem.text()).toBe(setting.i18n);
|
expect(sidebarItem.text()).toBe(setting.i18n);
|
||||||
|
|
||||||
const link = sidebarItem.find('*');
|
const link = sidebarItem.find('*');
|
||||||
expect(link.attributes('href')).toBe(`/settings/${setting.name}`);
|
expect(link.attributes('href')).toBe(
|
||||||
expect(link.classes('active')).toBe(setting.name === currentSection);
|
`/settings/${setting.name}`,
|
||||||
|
);
|
||||||
|
expect(link.classes('active')).toBe(
|
||||||
|
setting.name === currentSection,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -257,7 +261,9 @@ describe('SettingsView', () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
const mainContent = wrapper.find('.settings-main-content');
|
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(mainContent.exists()).toBe(false);
|
||||||
expect(mainContentNoSection.exists()).toBe(true);
|
expect(mainContentNoSection.exists()).toBe(true);
|
||||||
@@ -268,7 +274,9 @@ describe('SettingsView', () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
const mainContent = wrapper.find('.settings-main-content');
|
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(mainContent.exists()).toBe(true);
|
||||||
expect(mainContentNoSection.exists()).toBe(false);
|
expect(mainContentNoSection.exists()).toBe(false);
|
||||||
@@ -279,4 +287,4 @@ describe('SettingsView', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentSection = originalCurrentSection;
|
currentSection = originalCurrentSection;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ limitations under the License.
|
|||||||
import SidebarExpandButton from './SidebarExpandButton.vue';
|
import SidebarExpandButton from './SidebarExpandButton.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
'expanded': {
|
expanded: {
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean
|
type: Boolean,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['toggleExpanded']);
|
const emit = defineEmits(['toggleExpanded']);
|
||||||
|
|
||||||
const toggleSidebar = function toggleSidebar () {
|
const toggleSidebar = function toggleSidebar() {
|
||||||
emit('toggleExpanded');
|
emit('toggleExpanded');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -34,7 +34,10 @@ const toggleSidebar = function toggleSidebar () {
|
|||||||
<template>
|
<template>
|
||||||
<nav class="sidebar" :class="{ expanded }">
|
<nav class="sidebar" :class="{ expanded }">
|
||||||
<div class="sidebar-controls">
|
<div class="sidebar-controls">
|
||||||
<SidebarExpandButton @click="toggleSidebar" class="sidebar-expand-button" />
|
<SidebarExpandButton
|
||||||
|
@click="toggleSidebar"
|
||||||
|
class="sidebar-expand-button"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content" v-show="props.expanded">
|
<div class="sidebar-content" v-show="props.expanded">
|
||||||
<slot />
|
<slot />
|
||||||
@@ -74,4 +77,4 @@ const toggleSidebar = function toggleSidebar () {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -15,18 +15,18 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Icon from '@/features/icons/components/Icon.vue';
|
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['click']);
|
const emit = defineEmits(['click']);
|
||||||
|
|
||||||
const click = function click (event) {
|
const click = function click(event) {
|
||||||
emit('click', event);
|
emit('click', event);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button class="button sidebar-expand-button">
|
<button class="button sidebar-expand-button">
|
||||||
<Icon name="sidebar-expand" @click="click" />
|
<UIIcon name="sidebar-expand" @click="click" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,4 +38,4 @@ const click = function click (event) {
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,29 +18,19 @@ import { createI18n } from 'vue-i18n';
|
|||||||
|
|
||||||
export const fallbackLocale = 'en';
|
export const fallbackLocale = 'en';
|
||||||
|
|
||||||
|
export const LANGUAGES_RTL = ['ar'];
|
||||||
export const LANGUAGES_RTL = [
|
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt', 'it'];
|
||||||
'ar'
|
|
||||||
];
|
|
||||||
export const SUPPORTED_LANGUAGES = [
|
|
||||||
'en',
|
|
||||||
'de',
|
|
||||||
'fr',
|
|
||||||
'es',
|
|
||||||
'pt',
|
|
||||||
'it'
|
|
||||||
];
|
|
||||||
|
|
||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: fallbackLocale,
|
locale: fallbackLocale,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale,
|
||||||
messages: {}
|
messages: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadedLanguages = new Set();
|
const loadedLanguages = new Set();
|
||||||
|
|
||||||
export async function loadLanguage (locale) {
|
export const loadLanguage = async function loadLanguage(locale) {
|
||||||
if (!SUPPORTED_LANGUAGES.includes(locale)) {
|
if (!SUPPORTED_LANGUAGES.includes(locale)) {
|
||||||
locale = fallbackLocale;
|
locale = fallbackLocale;
|
||||||
}
|
}
|
||||||
@@ -48,7 +38,7 @@ export async function loadLanguage (locale) {
|
|||||||
if (loadedLanguages.has(locale)) {
|
if (loadedLanguages.has(locale)) {
|
||||||
i18n.global.locale.value = locale;
|
i18n.global.locale.value = locale;
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
const messages = (await import(`./locales/${locale}.json`)).default;
|
const messages = (await import(`./locales/${locale}.json`)).default;
|
||||||
|
|
||||||
@@ -58,8 +48,10 @@ export async function loadLanguage (locale) {
|
|||||||
loadedLanguages.add(locale);
|
loadedLanguages.add(locale);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setLocale = async function setLocale (locale) {
|
export const setLocale = async function setLocale(locale) {
|
||||||
await loadLanguage(locale);
|
await loadLanguage(locale);
|
||||||
document.documentElement.lang = locale;
|
document.documentElement.lang = locale;
|
||||||
document.documentElement.dir = LANGUAGES_RTL.includes(locale) ? 'rtl' : 'ltr';
|
document.documentElement.dir = LANGUAGES_RTL.includes(locale)
|
||||||
|
? 'rtl'
|
||||||
|
: 'ltr';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ limitations under the License.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup>
|
<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 SidebarExpandButton from '@/features/sidebar/components/SidebarExpandButton.vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
'expanded': {
|
expanded: {
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean
|
type: Boolean,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const expanded = ref(props.expanded);
|
const expanded = ref(props.expanded);
|
||||||
|
|
||||||
const toggleSidebar = function toggleSidebar () {
|
const toggleSidebar = function toggleSidebar() {
|
||||||
expanded.value = !expanded.value;
|
expanded.value = !expanded.value;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -37,11 +37,18 @@ const toggleSidebar = function toggleSidebar () {
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-container" :class="{ expanded }">
|
<div class="layout-container" :class="{ expanded }">
|
||||||
<div class="sidebar-expand-button-container">
|
<div class="sidebar-expand-button-container">
|
||||||
<SidebarExpandButton class="sidebar-expand-button" @click="toggleSidebar" />
|
<SidebarExpandButton
|
||||||
|
class="sidebar-expand-button"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Sidebar @toggle-expanded="toggleSidebar" :expanded="expanded" class="sidebar">
|
<AppSidebar
|
||||||
|
@toggle-expanded="toggleSidebar"
|
||||||
|
:expanded="expanded"
|
||||||
|
class="sidebar"
|
||||||
|
>
|
||||||
<slot name="sidebar" :toggleSidebar="toggleSidebar" />
|
<slot name="sidebar" :toggleSidebar="toggleSidebar" />
|
||||||
</Sidebar>
|
</AppSidebar>
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<slot :toggleSidebar="toggleSidebar" />
|
<slot :toggleSidebar="toggleSidebar" />
|
||||||
</main>
|
</main>
|
||||||
@@ -54,8 +61,8 @@ const toggleSidebar = function toggleSidebar () {
|
|||||||
grid-template-columns: calc(32px + 2em) 1fr;
|
grid-template-columns: calc(32px + 2em) 1fr;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"sidebar-expand sidebar-expand"
|
'sidebar-expand sidebar-expand'
|
||||||
"sidebar main-content";
|
'sidebar main-content';
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +87,7 @@ const toggleSidebar = function toggleSidebar () {
|
|||||||
.layout-container.expanded {
|
.layout-container.expanded {
|
||||||
grid-template-columns: 1fr 0;
|
grid-template-columns: 1fr 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-container:not(.expanded) .sidebar-expand-button-container {
|
.layout-container:not(.expanded) .sidebar-expand-button-container {
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -100,4 +107,4 @@ const toggleSidebar = function toggleSidebar () {
|
|||||||
grid-area: main-content;
|
grid-area: main-content;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -91,4 +91,4 @@
|
|||||||
"loadError": "Das Impressum konnte nicht geladen werden."
|
"loadError": "Das Impressum konnte nicht geladen werden."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,4 @@
|
|||||||
"loadError": "The legal notice could not be loaded."
|
"loadError": "The legal notice could not be loaded."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"input": {
|
"input": {
|
||||||
"done": {
|
"done": {
|
||||||
"title": "Salva valore",
|
"title": "Salva valore",
|
||||||
"ariaLabel": "Salva il valore inserito"
|
"ariaLabel": "Salva il valore inserito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,4 +62,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"input": {
|
"input": {
|
||||||
"done": {
|
"done": {
|
||||||
"title": "Guardar valor",
|
"title": "Guardar valor",
|
||||||
"ariaLabel": "Guarda o valor introduzido"
|
"ariaLabel": "Guarda o valor introduzido"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,4 +62,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,40 +14,48 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getAssetsUrl } from './config/env';
|
import { i18n, loadLanguage, fallbackLocale } from './i18n';
|
||||||
import { createApp } from 'vue'
|
import router from './router';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
import App from './App.vue'
|
import { createApp } from 'vue';
|
||||||
import { i18n, loadLanguage, fallbackLocale } from './i18n';
|
|
||||||
import router from './router'
|
import App from './App.vue';
|
||||||
import './styles/common.css'
|
|
||||||
import './styles/variables/colors.css'
|
import './styles/common.css';
|
||||||
|
import rawFonts from './styles/fonts.json';
|
||||||
|
import './styles/variables/colors.css';
|
||||||
import './styles/variables/dimensions.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 settings from './features/settings/config.js';
|
||||||
|
|
||||||
|
import { getAssetsUrl } from './config/env';
|
||||||
import { validateSettingsConfig } from './features/settings/utils/settingsValidator';
|
import { validateSettingsConfig } from './features/settings/utils/settingsValidator';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const fonts = rawFonts.map((font) => new FontFace(
|
const fonts = rawFonts.map(
|
||||||
font.family ?? '',
|
(font) =>
|
||||||
Array.isArray(font.source)
|
new FontFace(
|
||||||
? font.source.map(
|
font.family ?? '',
|
||||||
(source) => `url('${source.url
|
Array.isArray(font.source)
|
||||||
? new URL(source.url, getAssetsUrl()).href
|
? font.source
|
||||||
: ''
|
.map(
|
||||||
}') format('${source.format ?? ''}')`
|
(source) =>
|
||||||
).join(',')
|
`url('${
|
||||||
: '',
|
source.url
|
||||||
{
|
? new URL(source.url, getAssetsUrl())
|
||||||
weight: font.weight ?? undefined,
|
.href
|
||||||
style: font.style ?? undefined
|
: ''
|
||||||
}
|
}') format('${source.format ?? ''}')`,
|
||||||
));
|
)
|
||||||
|
.join(',')
|
||||||
|
: '',
|
||||||
|
{
|
||||||
|
weight: font.weight ?? undefined,
|
||||||
|
style: font.style ?? undefined,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
await Promise.all(fonts.map((font) => font.load()));
|
await Promise.all(fonts.map((font) => font.load()));
|
||||||
fonts.forEach((font) => document.fonts.add(font));
|
fonts.forEach((font) => document.fonts.add(font));
|
||||||
|
|
||||||
@@ -57,10 +65,6 @@ import { validateSettingsConfig } from './features/settings/utils/settingsValida
|
|||||||
pinia.use(piniaPluginPersistedstate);
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
validateSettingsConfig(settings);
|
validateSettingsConfig(settings);
|
||||||
|
|
||||||
createApp(App)
|
createApp(App).use(router).use(i18n).use(pinia).mount('#app');
|
||||||
.use(router)
|
|
||||||
.use(i18n)
|
|
||||||
.use(pinia)
|
|
||||||
.mount('#app');
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -14,71 +14,71 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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 LegalNoticeView from '@/features/legal/views/LegalNoticeView.vue';
|
||||||
import SearchResultsView from '@/features/search/views/SearchResultsView.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 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 = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'startPage',
|
name: 'startPage',
|
||||||
component: SearchView
|
component: SearchView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/search',
|
path: '/search',
|
||||||
name: 'searchResults',
|
name: 'searchResults',
|
||||||
component: SearchResultsView,
|
component: SearchResultsView,
|
||||||
props: route => ({
|
props: (route) => ({
|
||||||
searchQuery: route.query.q
|
searchQuery: route.query.q,
|
||||||
}),
|
}),
|
||||||
meta: {
|
meta: {
|
||||||
title: (route) => route.query.q
|
title: (route) => route.query.q,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/legal/notice',
|
path: '/legal/notice',
|
||||||
name: 'legalNotice',
|
name: 'legalNotice',
|
||||||
component: LegalNoticeView,
|
component: LegalNoticeView,
|
||||||
meta: {
|
meta: {
|
||||||
title: () => i18n.global.t('legal.notice.title')
|
title: () => i18n.global.t('legal.notice.title'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/:rest(.*)*',
|
path: '/settings/:rest(.*)*',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
component: SettingsView,
|
component: SettingsView,
|
||||||
meta: {
|
meta: {
|
||||||
title: () => i18n.global.t('preferences.settings')
|
title: () => i18n.global.t('preferences.settings'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'notFound',
|
name: 'notFound',
|
||||||
component: NotFound
|
component: NotFound,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updatePageTitle = function updatePageTitle (route) {
|
export const updatePageTitle = function updatePageTitle(route) {
|
||||||
const title =
|
const title =
|
||||||
typeof route.meta.title === 'function'
|
typeof route.meta.title === 'function'
|
||||||
? route.meta.title(route)
|
? route.meta.title(route)
|
||||||
: route.meta.title;
|
: route.meta.title;
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
document.title = `${title} - Seekra`;
|
document.title = `${title} - Seekra`;
|
||||||
} else {
|
} else {
|
||||||
document.title = 'Seekra';
|
document.title = 'Seekra';
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove trailing slash(es)
|
// remove trailing slash(es)
|
||||||
@@ -88,9 +88,9 @@ router.beforeEach((route) => {
|
|||||||
path: route.path.replace(/\/+$/, ''),
|
path: route.path.replace(/\/+$/, ''),
|
||||||
query: route.query,
|
query: route.query,
|
||||||
hash: route.hash,
|
hash: route.hash,
|
||||||
replace: true
|
replace: true,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -19,7 +19,19 @@ body {
|
|||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
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 {
|
.link {
|
||||||
@@ -27,7 +39,8 @@ body {
|
|||||||
color: var(--dark);
|
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;
|
outline: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,144 +15,144 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--primary-color-l-8: oklch(0.897 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-7: oklch(0.847 0.202 260);
|
||||||
--primary-color-l-6: oklch(0.797 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-6: oklch(0.747 0.202 260);
|
||||||
--primary-color-l-4: oklch(0.697 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-3: oklch(0.647 0.202 260);
|
||||||
--primary-color-l-2: oklch(0.597 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-l-1: oklch(0.547 0.202 260);
|
||||||
--primary-color: oklch(0.497 0.202 260);
|
--primary-color: oklch(0.497 0.202 260);
|
||||||
--primary-color-d-1: oklch(0.447 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-2: oklch(0.397 0.202 260);
|
||||||
--primary-color-d-3: oklch(0.347 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-4: oklch(0.297 0.202 260);
|
||||||
--primary-color-d-5: oklch(0.247 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-6: oklch(0.197 0.202 260);
|
||||||
--primary-color-d-7: oklch(0.147 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-d-8: oklch(0.097 0.202 260);
|
||||||
|
|
||||||
--black-l-8: oklch(0.4 0 0);
|
--black-l-8: oklch(0.4 0 0);
|
||||||
--black-l-7: oklch(0.35 0 0);
|
--black-l-7: oklch(0.35 0 0);
|
||||||
--black-l-6: oklch(0.3 0 0);
|
--black-l-6: oklch(0.3 0 0);
|
||||||
--black-l-5: oklch(0.25 0 0);
|
--black-l-5: oklch(0.25 0 0);
|
||||||
--black-l-4: oklch(0.2 0 0);
|
--black-l-4: oklch(0.2 0 0);
|
||||||
--black-l-3: oklch(0.15 0 0);
|
--black-l-3: oklch(0.15 0 0);
|
||||||
--black-l-2: oklch(0.1 0 0);
|
--black-l-2: oklch(0.1 0 0);
|
||||||
--black-l-1: oklch(0.05 0 0);
|
--black-l-1: oklch(0.05 0 0);
|
||||||
--black: oklch(0 0 0);
|
--black: oklch(0 0 0);
|
||||||
|
|
||||||
--white: oklch(1 0 0);
|
--white: oklch(1 0 0);
|
||||||
--white-d-1: oklch(0.95 0 0);
|
--white-d-1: oklch(0.95 0 0);
|
||||||
--white-d-2: oklch(0.9 0 0);
|
--white-d-2: oklch(0.9 0 0);
|
||||||
--white-d-3: oklch(0.85 0 0);
|
--white-d-3: oklch(0.85 0 0);
|
||||||
--white-d-4: oklch(0.8 0 0);
|
--white-d-4: oklch(0.8 0 0);
|
||||||
--white-d-5: oklch(0.75 0 0);
|
--white-d-5: oklch(0.75 0 0);
|
||||||
--white-d-6: oklch(0.7 0 0);
|
--white-d-6: oklch(0.7 0 0);
|
||||||
--white-d-7: oklch(0.65 0 0);
|
--white-d-7: oklch(0.65 0 0);
|
||||||
--white-d-8: oklch(0.6 0 0);
|
--white-d-8: oklch(0.6 0 0);
|
||||||
|
|
||||||
--dark-l-8: var(--black-l-8);
|
--dark-l-8: var(--black-l-8);
|
||||||
--dark-l-7: var(--black-l-7);
|
--dark-l-7: var(--black-l-7);
|
||||||
--dark-l-6: var(--black-l-6);
|
--dark-l-6: var(--black-l-6);
|
||||||
--dark-l-5: var(--black-l-5);
|
--dark-l-5: var(--black-l-5);
|
||||||
--dark-l-4: var(--black-l-4);
|
--dark-l-4: var(--black-l-4);
|
||||||
--dark-l-3: var(--black-l-3);
|
--dark-l-3: var(--black-l-3);
|
||||||
--dark-l-2: var(--black-l-2);
|
--dark-l-2: var(--black-l-2);
|
||||||
--dark-l-1: var(--black-l-1);
|
--dark-l-1: var(--black-l-1);
|
||||||
--dark: var(--black);
|
--dark: var(--black);
|
||||||
|
|
||||||
--light: var(--white);
|
--light: var(--white);
|
||||||
--light-d-1: var(--white-d-1);
|
--light-d-1: var(--white-d-1);
|
||||||
--light-d-2: var(--white-d-2);
|
--light-d-2: var(--white-d-2);
|
||||||
--light-d-3: var(--white-d-3);
|
--light-d-3: var(--white-d-3);
|
||||||
--light-d-4: var(--white-d-4);
|
--light-d-4: var(--white-d-4);
|
||||||
--light-d-5: var(--white-d-5);
|
--light-d-5: var(--white-d-5);
|
||||||
--light-d-6: var(--white-d-6);
|
--light-d-6: var(--white-d-6);
|
||||||
--light-d-7: var(--white-d-7);
|
--light-d-7: var(--white-d-7);
|
||||||
--light-d-8: var(--white-d-8);
|
--light-d-8: var(--white-d-8);
|
||||||
|
|
||||||
--dark-bg: var(--black-l-2);
|
--dark-bg: var(--black-l-2);
|
||||||
--light-bg: var(--white);
|
--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);
|
--gray-box-shadow: oklch(0.8 0.0001 271 / 0.7);
|
||||||
--blue-box-shadow: oklch(0.52 0.15 268 / 0.25);
|
--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);
|
--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);
|
--color-negative-border: oklch(51% 0.22706 29.244);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body.color-scheme-auto {
|
body.color-scheme-auto {
|
||||||
--dark-l-8: var(--white-d-8);
|
--dark-l-8: var(--white-d-8);
|
||||||
--dark-l-7: var(--white-d-7);
|
--dark-l-7: var(--white-d-7);
|
||||||
--dark-l-6: var(--white-d-6);
|
--dark-l-6: var(--white-d-6);
|
||||||
--dark-l-5: var(--white-d-5);
|
--dark-l-5: var(--white-d-5);
|
||||||
--dark-l-4: var(--white-d-4);
|
--dark-l-4: var(--white-d-4);
|
||||||
--dark-l-3: var(--white-d-3);
|
--dark-l-3: var(--white-d-3);
|
||||||
--dark-l-2: var(--white-d-2);
|
--dark-l-2: var(--white-d-2);
|
||||||
--dark-l-1: var(--white-d-1);
|
--dark-l-1: var(--white-d-1);
|
||||||
--dark: var(--white);
|
--dark: var(--white);
|
||||||
|
|
||||||
--light: var(--black);
|
--light: var(--black);
|
||||||
--light-d-1: oklch(0.10 0 0);
|
--light-d-1: oklch(0.1 0 0);
|
||||||
--light-d-2: var(--black-l-2);
|
--light-d-2: var(--black-l-2);
|
||||||
--light-d-3: var(--black-l-3);
|
--light-d-3: var(--black-l-3);
|
||||||
--light-d-4: var(--black-l-4);
|
--light-d-4: var(--black-l-4);
|
||||||
--light-d-5: var(--black-l-5);
|
--light-d-5: var(--black-l-5);
|
||||||
--light-d-6: var(--black-l-6);
|
--light-d-6: var(--black-l-6);
|
||||||
--light-d-7: var(--black-l-7);
|
--light-d-7: var(--black-l-7);
|
||||||
--light-d-8: var(--black-l-8);
|
--light-d-8: var(--black-l-8);
|
||||||
|
|
||||||
--dark-bg: var(--white);
|
--dark-bg: var(--white);
|
||||||
--light-bg: oklch(0.18 0 0);
|
--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);
|
--invert: invert(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark {
|
body.dark {
|
||||||
--dark-l-8: var(--white-d-8);
|
--dark-l-8: var(--white-d-8);
|
||||||
--dark-l-7: var(--white-d-7);
|
--dark-l-7: var(--white-d-7);
|
||||||
--dark-l-6: var(--white-d-6);
|
--dark-l-6: var(--white-d-6);
|
||||||
--dark-l-5: var(--white-d-5);
|
--dark-l-5: var(--white-d-5);
|
||||||
--dark-l-4: var(--white-d-4);
|
--dark-l-4: var(--white-d-4);
|
||||||
--dark-l-3: var(--white-d-3);
|
--dark-l-3: var(--white-d-3);
|
||||||
--dark-l-2: var(--white-d-2);
|
--dark-l-2: var(--white-d-2);
|
||||||
--dark-l-1: var(--white-d-1);
|
--dark-l-1: var(--white-d-1);
|
||||||
--dark: var(--white);
|
--dark: var(--white);
|
||||||
|
|
||||||
--light: var(--black);
|
--light: var(--black);
|
||||||
--light-d-1: oklch(0.10 0 0);
|
--light-d-1: oklch(0.1 0 0);
|
||||||
--light-d-2: var(--black-l-2);
|
--light-d-2: var(--black-l-2);
|
||||||
--light-d-3: var(--black-l-3);
|
--light-d-3: var(--black-l-3);
|
||||||
--light-d-4: var(--black-l-4);
|
--light-d-4: var(--black-l-4);
|
||||||
--light-d-5: var(--black-l-5);
|
--light-d-5: var(--black-l-5);
|
||||||
--light-d-6: var(--black-l-6);
|
--light-d-6: var(--black-l-6);
|
||||||
--light-d-7: var(--black-l-7);
|
--light-d-7: var(--black-l-7);
|
||||||
--light-d-8: var(--black-l-8);
|
--light-d-8: var(--black-l-8);
|
||||||
|
|
||||||
--dark-bg: var(--white);
|
--dark-bg: var(--white);
|
||||||
--light-bg: oklch(0.18 0 0);
|
--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);
|
--invert: invert(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
:root {
|
:root {
|
||||||
--input-padding: 0.7em;
|
--input-padding: 0.7em;
|
||||||
--input-height: 1em;
|
--input-height: 1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,34 +15,37 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { vi } from 'vitest';
|
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { vi } from 'vitest';
|
||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
import { createMemoryHistory, createRouter } from 'vue-router';
|
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||||
|
|
||||||
export const mountComponent = function mountComponent (component, options = {}, i18nMessages = {}, routes = [], piniaOptions = {}) {
|
export const mountComponent = function mountComponent(
|
||||||
const {
|
component,
|
||||||
setupStores,
|
options = {},
|
||||||
...testingPiniaOptions
|
i18nMessages = {},
|
||||||
} = piniaOptions;
|
routes = [],
|
||||||
|
piniaOptions = {},
|
||||||
|
) {
|
||||||
|
const { setupStores, ...testingPiniaOptions } = piniaOptions;
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
messages: {
|
messages: {
|
||||||
en: i18nMessages
|
en: i18nMessages,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createMemoryHistory(),
|
history: createMemoryHistory(),
|
||||||
routes
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const testingPinia = createTestingPinia({
|
const testingPinia = createTestingPinia({
|
||||||
...testingPiniaOptions,
|
...testingPiniaOptions,
|
||||||
stubActions: false,
|
stubActions: false,
|
||||||
createSpy: vi.fn
|
createSpy: vi.fn,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (setupStores) {
|
if (setupStores) {
|
||||||
@@ -51,12 +54,8 @@ export const mountComponent = function mountComponent (component, options = {},
|
|||||||
|
|
||||||
return mount(component, {
|
return mount(component, {
|
||||||
global: {
|
global: {
|
||||||
plugins: [
|
plugins: [i18n, router, testingPinia],
|
||||||
i18n,
|
|
||||||
router,
|
|
||||||
testingPinia
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
...options
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,111 +14,123 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
import { ensureUnit } from '../cssDimensions';
|
import { ensureUnit } from '../cssDimensions';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
describe('cssDimensions', () => {
|
describe('cssDimensions', () => {
|
||||||
test.for([
|
test.for([
|
||||||
{ dimension: null, expected: '0px' },
|
{ dimension: null, expected: '0px' },
|
||||||
{ dimension: undefined, expected: '0px' },
|
{ dimension: undefined, expected: '0px' },
|
||||||
|
|
||||||
{ dimension: 0, expected: '0px' },
|
{ dimension: 0, expected: '0px' },
|
||||||
{ dimension: -0, expected: '0px' },
|
{ dimension: -0, expected: '0px' },
|
||||||
{ dimension: -1, expected: '-1px' },
|
{ dimension: -1, expected: '-1px' },
|
||||||
{ dimension: -42, expected: '-42px' },
|
{ dimension: -42, expected: '-42px' },
|
||||||
{ dimension: 1, expected: '1px' },
|
{ dimension: 1, expected: '1px' },
|
||||||
{ dimension: 42, expected: '42px' },
|
{ dimension: 42, expected: '42px' },
|
||||||
|
|
||||||
{ dimension: 0.0, expected: '0px' },
|
{ dimension: 0.0, expected: '0px' },
|
||||||
{ dimension: -0.0, expected: '0px' },
|
{ dimension: -0.0, expected: '0px' },
|
||||||
{ dimension: -1.0, expected: '-1px' },
|
{ dimension: -1.0, expected: '-1px' },
|
||||||
{ dimension: -42.0, expected: '-42px' },
|
{ dimension: -42.0, expected: '-42px' },
|
||||||
{ dimension: 1.0, expected: '1px' },
|
{ dimension: 1.0, expected: '1px' },
|
||||||
{ dimension: 42.0, expected: '42px' },
|
{ dimension: 42.0, expected: '42px' },
|
||||||
|
|
||||||
{ dimension: 0.42, expected: '0.42px' },
|
{ dimension: 0.42, expected: '0.42px' },
|
||||||
{ dimension: -0.42, expected: '-0.42px' },
|
{ dimension: -0.42, expected: '-0.42px' },
|
||||||
{ dimension: -1.42, expected: '-1.42px' },
|
{ dimension: -1.42, expected: '-1.42px' },
|
||||||
{ dimension: -42.42, expected: '-42.42px' },
|
{ dimension: -42.42, expected: '-42.42px' },
|
||||||
{ dimension: 1.42, expected: '1.42px' },
|
{ dimension: 1.42, expected: '1.42px' },
|
||||||
{ dimension: 42.42, expected: '42.42px' },
|
{ dimension: 42.42, expected: '42.42px' },
|
||||||
|
|
||||||
{ dimension: '0', expected: '0px' },
|
{ dimension: '0', expected: '0px' },
|
||||||
{ dimension: '-0', expected: '-0px' },
|
{ dimension: '-0', expected: '-0px' },
|
||||||
{ dimension: '-1', expected: '-1px' },
|
{ dimension: '-1', expected: '-1px' },
|
||||||
{ dimension: '-42', expected: '-42px' },
|
{ dimension: '-42', expected: '-42px' },
|
||||||
{ dimension: '1', expected: '1px' },
|
{ dimension: '1', expected: '1px' },
|
||||||
{ dimension: '42', expected: '42px' },
|
{ dimension: '42', expected: '42px' },
|
||||||
|
|
||||||
{ dimension: '0.0', expected: '0.0px' },
|
{ dimension: '0.0', expected: '0.0px' },
|
||||||
{ dimension: '-0.0', expected: '-0.0px' },
|
{ dimension: '-0.0', expected: '-0.0px' },
|
||||||
{ dimension: '-1.0', expected: '-1.0px' },
|
{ dimension: '-1.0', expected: '-1.0px' },
|
||||||
{ dimension: '-42.0', expected: '-42.0px' },
|
{ dimension: '-42.0', expected: '-42.0px' },
|
||||||
{ dimension: '1.0', expected: '1.0px' },
|
{ dimension: '1.0', expected: '1.0px' },
|
||||||
{ dimension: '42.0', expected: '42.0px' },
|
{ dimension: '42.0', expected: '42.0px' },
|
||||||
|
|
||||||
{ dimension: '0.42', expected: '0.42px' },
|
{ dimension: '0.42', expected: '0.42px' },
|
||||||
{ dimension: '-0.42', expected: '-0.42px' },
|
{ dimension: '-0.42', expected: '-0.42px' },
|
||||||
{ dimension: '-1.42', expected: '-1.42px' },
|
{ dimension: '-1.42', expected: '-1.42px' },
|
||||||
{ dimension: '-42.42', expected: '-42.42px' },
|
{ dimension: '-42.42', expected: '-42.42px' },
|
||||||
{ dimension: '1.42', expected: '1.42px' },
|
{ dimension: '1.42', expected: '1.42px' },
|
||||||
{ dimension: '42.42', expected: '42.42px' },
|
{ dimension: '42.42', expected: '42.42px' },
|
||||||
|
|
||||||
{ dimension: '0px', expected: '0px' },
|
{ dimension: '0px', expected: '0px' },
|
||||||
{ dimension: '-0px', expected: '-0px' },
|
{ dimension: '-0px', expected: '-0px' },
|
||||||
{ dimension: '-1px', expected: '-1px' },
|
{ dimension: '-1px', expected: '-1px' },
|
||||||
{ dimension: '-42px', expected: '-42px' },
|
{ dimension: '-42px', expected: '-42px' },
|
||||||
{ dimension: '1px', expected: '1px' },
|
{ dimension: '1px', expected: '1px' },
|
||||||
{ dimension: '42px', expected: '42px' },
|
{ dimension: '42px', expected: '42px' },
|
||||||
|
|
||||||
{ dimension: '0.0px', expected: '0.0px' },
|
{ dimension: '0.0px', expected: '0.0px' },
|
||||||
{ dimension: '-0.0px', expected: '-0.0px' },
|
{ dimension: '-0.0px', expected: '-0.0px' },
|
||||||
{ dimension: '-1.0px', expected: '-1.0px' },
|
{ dimension: '-1.0px', expected: '-1.0px' },
|
||||||
{ dimension: '-42.0px', expected: '-42.0px' },
|
{ dimension: '-42.0px', expected: '-42.0px' },
|
||||||
{ dimension: '1.0px', expected: '1.0px' },
|
{ dimension: '1.0px', expected: '1.0px' },
|
||||||
{ dimension: '42.0px', expected: '42.0px' },
|
{ dimension: '42.0px', expected: '42.0px' },
|
||||||
|
|
||||||
{ dimension: '0.42px', expected: '0.42px' },
|
{ dimension: '0.42px', expected: '0.42px' },
|
||||||
{ dimension: '-0.42px', expected: '-0.42px' },
|
{ dimension: '-0.42px', expected: '-0.42px' },
|
||||||
{ dimension: '-1.42px', expected: '-1.42px' },
|
{ dimension: '-1.42px', expected: '-1.42px' },
|
||||||
{ dimension: '-42.42px', expected: '-42.42px' },
|
{ dimension: '-42.42px', expected: '-42.42px' },
|
||||||
{ dimension: '1.42px', expected: '1.42px' },
|
{ dimension: '1.42px', expected: '1.42px' },
|
||||||
{ dimension: '42.42px', expected: '42.42px' },
|
{ dimension: '42.42px', expected: '42.42px' },
|
||||||
|
|
||||||
{ dimension: '0em', expected: '0em' },
|
{ dimension: '0em', expected: '0em' },
|
||||||
{ dimension: '-0em', expected: '-0em' },
|
{ dimension: '-0em', expected: '-0em' },
|
||||||
{ dimension: '-1em', expected: '-1em' },
|
{ dimension: '-1em', expected: '-1em' },
|
||||||
{ dimension: '-42em', expected: '-42em' },
|
{ dimension: '-42em', expected: '-42em' },
|
||||||
{ dimension: '1em', expected: '1em' },
|
{ dimension: '1em', expected: '1em' },
|
||||||
{ dimension: '42em', expected: '42em' },
|
{ dimension: '42em', expected: '42em' },
|
||||||
|
|
||||||
{ dimension: '0.0em', expected: '0.0em' },
|
{ dimension: '0.0em', expected: '0.0em' },
|
||||||
{ dimension: '-0.0em', expected: '-0.0em' },
|
{ dimension: '-0.0em', expected: '-0.0em' },
|
||||||
{ dimension: '-1.0em', expected: '-1.0em' },
|
{ dimension: '-1.0em', expected: '-1.0em' },
|
||||||
{ dimension: '-42.0em', expected: '-42.0em' },
|
{ dimension: '-42.0em', expected: '-42.0em' },
|
||||||
{ dimension: '1.0em', expected: '1.0em' },
|
{ dimension: '1.0em', expected: '1.0em' },
|
||||||
{ dimension: '42.0em', expected: '42.0em' },
|
{ dimension: '42.0em', expected: '42.0em' },
|
||||||
|
|
||||||
{ dimension: '0.42em', expected: '0.42em' },
|
{ dimension: '0.42em', expected: '0.42em' },
|
||||||
{ dimension: '-0.42em', expected: '-0.42em' },
|
{ dimension: '-0.42em', expected: '-0.42em' },
|
||||||
{ dimension: '-1.42em', expected: '-1.42em' },
|
{ dimension: '-1.42em', expected: '-1.42em' },
|
||||||
{ dimension: '-42.42em', expected: '-42.42em' },
|
{ dimension: '-42.42em', expected: '-42.42em' },
|
||||||
{ dimension: '1.42em', expected: '1.42em' },
|
{ dimension: '1.42em', expected: '1.42em' },
|
||||||
{ dimension: '42.42em', expected: '42.42em' },
|
{ dimension: '42.42em', expected: '42.42em' },
|
||||||
|
|
||||||
{ dimension: 'calc(42px - 1em)', expected: 'calc(42px - 1em)' },
|
{ dimension: 'calc(42px - 1em)', expected: 'calc(42px - 1em)' },
|
||||||
{ dimension: 'calc(42px)', expected: 'calc(42px)' },
|
{ dimension: 'calc(42px)', expected: 'calc(42px)' },
|
||||||
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
|
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
|
||||||
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
|
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
|
||||||
{ dimension: 'max(42px, 5rem)', expected: 'max(42px, 5rem)' },
|
{ 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)',
|
||||||
|
},
|
||||||
|
|
||||||
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
|
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
|
||||||
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
|
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
|
||||||
{ dimension: 'max(42px,5rem)', expected: 'max(42px,5rem)' },
|
{ 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 )' }
|
dimension: 'clamp(42vh,23vw,13cap)',
|
||||||
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
|
expected: 'clamp(42vh,23vw,13cap)',
|
||||||
expect(ensureUnit(dimension)).toBe(expected);
|
},
|
||||||
});
|
{
|
||||||
});
|
dimension: 'clamp( 42vh,23vw,13cap )',
|
||||||
|
expected: 'clamp( 42vh,23vw,13cap )',
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'ensureUnit returns $expected with input $dimension',
|
||||||
|
({ dimension, expected }) => {
|
||||||
|
expect(ensureUnit(dimension)).toBe(expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const ensureUnit = function (d) {
|
export const ensureUnit = function ensureUnit(d) {
|
||||||
if (!d) d = 0;
|
if (!d) d = 0;
|
||||||
|
|
||||||
// ensure that it is a string
|
// ensure that it is a string
|
||||||
@@ -25,7 +25,7 @@ export const ensureUnit = function (d) {
|
|||||||
// if it ends with a number
|
// if it ends with a number
|
||||||
if (/\d$/.test(d)) {
|
if (/\d$/.test(d)) {
|
||||||
d += 'px';
|
d += 'px';
|
||||||
};
|
}
|
||||||
|
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="not-found main-content-padding">
|
<div class="not-found main-content-padding">
|
||||||
<span class="error-message">
|
<span class="error-message">
|
||||||
{{ t('error.pageNotFound') }}
|
{{ t('error.pageNotFound') }}
|
||||||
</span>
|
</span>
|
||||||
<RouterLink to="/" id="link">
|
<RouterLink to="/" id="link">
|
||||||
@@ -40,7 +40,7 @@ const { t } = useI18n();
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.error-message{
|
.error-message {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
@@ -53,4 +53,4 @@ const { t } = useI18n();
|
|||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,29 +14,29 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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 path from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
target: 'es2020'
|
target: 'es2020',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
createHtmlPlugin({
|
createHtmlPlugin({
|
||||||
minify: true
|
minify: true,
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src')
|
'@': path.resolve(__dirname, './src'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
globals: false,
|
globals: false,
|
||||||
environment: 'jsdom'
|
environment: 'jsdom',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|||||||