Author SHA1 Message Date
jakob.scheidandGitea 59ffa4b043 chore(licenses): add license headers to setup scripts 2026-07-28 20:16:13 +02:00
29 changed files with 867 additions and 1707 deletions
-16
View File
@@ -1,16 +0,0 @@
# Copyright 2026 Seekra
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
VITE_ASSETS_BUILD_ID=2026-08-04-76a067e
VITE_ASSETS_BASE_URL=https://assets.seekra.jcloud-services.ddns.net
-38
View File
@@ -1,38 +0,0 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
stroke="currentColor"
stroke-width="2"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
d="
M6 4
h12
a4 4 0 0 1 4 4
v8
a4 4 0 0 1 -4 4
h-12
a4 4 0 0 1 -4 -4
v-8
a4 4 0 0 1 4 -4
m2 0
v16
"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-22
View File
@@ -1,22 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const getAssetsUrl = function getAssetsUrl () {
return new URL(
`${import.meta.env.VITE_ASSETS_BUILD_ID}/`,
import.meta.env.VITE_ASSETS_BASE_URL
).href;
};
+2 -41
View File
@@ -40,20 +40,8 @@ const copyrightPeriod =
</RouterLink> </RouterLink>
<LanguageSwitchButton /> <LanguageSwitchButton />
</div> </div>
<div class="footer-segment legal-segment"> <div class="footer-segment">
<div></div> &copy; {{ copyrightPeriod }} Seekra
<div class="copyright-notice">
&copy; {{ copyrightPeriod }} Seekra
</div>
<div class="legal-links">
<RouterLink
:to="{ name: 'legalNotice' }"
class="link"
:title="t('legal.notice.title')"
>
{{ t('legal.notice.title') }}
</RouterLink>
</div>
</div> </div>
</footer> </footer>
</template> </template>
@@ -75,31 +63,4 @@ const copyrightPeriod =
.global-footer a { .global-footer a {
color: var(--dark); color: var(--dark);
} }
.legal-segment {
display: grid;
grid-template-columns: 1fr auto 1fr;
text-align: center;
}
.copyright-notice {
justify-self: center;
}
.legal-links {
justify-self: end;
}
@media (max-width: 48rem) {
.legal-segment {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0;
}
.legal-links {
margin-top: 0.8em;
}
}
</style> </style>
@@ -39,7 +39,7 @@ vi.mock('@/features/icons/components/Icon.vue', () => ({
} }
})); }));
describe('LanguageSwitchButton', () => { describe('LanguageSwitchButton.vue', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
localStorage.clear(); localStorage.clear();
-22
View File
@@ -1,22 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export class FetchError extends Error {
constructor (message) {
super(message);
this.name = 'FetchError';
};
};
@@ -1,110 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchError } from '../../errors';
import { getLegalNotice } from '../fetchLegalContent';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
let fetchSpy;
beforeEach(() => {
fetchSpy = vi.spyOn(globalThis, 'fetch');
});
afterEach(() => {
fetchSpy.mockRestore();
});
const exampleHtml = `<h1>Heading</h1>
<p>
<a>...</a>
</p>`;
const exampleHtmlWithLinkClass = `<h1>Heading</h1>
<p>
<a class="link">...</a>
</p>`;
const exampleHtmlDocument = `<html>
<head>
<title>Legal Notice</title>
</head>
<body>
${exampleHtml}
</body>
</html>`;
const exampleHtmlDocumentWithMain = `<html>
<head>
<title>Legal Notice</title>
</head>
<body>
<main>
${exampleHtml}
</main>
</body>
</html>`;
describe('fetchLegalContent', () => {
describe('getLegalNotice', () => {
describe('success', () => {
test('returns HTML body', async () => {
fetchSpy.mockResolvedValue(
new Response(exampleHtmlDocument)
);
expect(
(await getLegalNotice()).trim()
).toBe(exampleHtmlWithLinkClass);
});
test('returns HTML <main>', async () => {
fetchSpy.mockResolvedValue(
new Response(exampleHtmlDocumentWithMain)
);
expect(
(await getLegalNotice()).trim()
).toBe(exampleHtmlWithLinkClass);
});
test('does not fail on success status code', async () => {
fetchSpy.mockResolvedValue(
new Response(null, { status: 206 })
);
await getLegalNotice();
});
});
describe('error', () => {
test('throws FetchError when fetch fails', async () => {
fetchSpy.mockRejectedValue(new TypeError());
await expect(getLegalNotice()).rejects.toThrow(FetchError);
});
test('throws FetchError on HTTP error', async () => {
fetchSpy.mockResolvedValue(
new Response(null, { status: 404 })
)
await expect(getLegalNotice()).rejects.toThrow(FetchError);
});
test('does not throw FetchError when fetch throws another error than TypeError', async () => {
fetchSpy.mockRejectedValue(new Error());
await expect(getLegalNotice()).rejects.toThrow(Error);
});
});
});
});
@@ -1,44 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchError } from '../errors';
import { getAssetsUrl } from '@/config/env';
const fetchHtml = async function fetchHtml (url, options) {
let response;
try {
response = await fetch(url, options);
} catch (err) {
if (err instanceof TypeError) throw new FetchError(err);
else throw err;
}
if (response.ok) return await response.text();
else throw new FetchError();
};
export const getLegalNotice = async function getLegalNotice () {
const legalNoticeUrl = new URL('legal/legal_notice.html', getAssetsUrl()).href;
const html = await fetchHtml(legalNoticeUrl);
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const legalNoticeContainer = doc.querySelector('main') ?? doc.body;
legalNoticeContainer.querySelectorAll('a').forEach(
(link) => link.classList.add('link')
);
return legalNoticeContainer.innerHTML;
};
@@ -1,61 +0,0 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import { getLegalNotice } from '../services/fetchLegalContent';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const loading = ref(true);
const legalNoticeError = ref(null);
const legalNoticeHtml = ref(null);
onMounted(async () => {
try {
legalNoticeHtml.value = await getLegalNotice();
} catch {
legalNoticeError.value = 'legal.notice.loadError';
} finally {
loading.value = false;
}
});
</script>
<template>
<div class="main-content-padding legal-notice-container">
<header class="header">
<h1>
{{ t('legal.notice.title') }}
</h1>
</header>
<main>
<div class="legal-notice-loading" v-if="loading">
{{ t('loading') }}
</div>
<div class="error" v-else-if="legalNoticeError">
{{ t(legalNoticeError) }}
</div>
<div class="legal-notice" v-html="legalNoticeHtml" v-else></div>
</main>
</div>
</template>
<style scoped>
.header h1 {
margin-top: 0;
}
</style>
@@ -1,78 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchError } from '../../errors.js';
import LegalNoticeView from '../LegalNoticeView.vue';
import { mountComponent } from '@/test-utils/mountComponent';
import { flushPromises } from '@vue/test-utils';
import { afterEach, describe, expect, test, vi } from 'vitest';
const getLegalNotice = vi.hoisted(() => vi.fn().mockResolvedValue('legal notice'));
vi.mock('../../services/fetchLegalContent', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
getLegalNotice
};
});
afterEach(() => getLegalNotice.mockRestore());
describe('LegalNoticeView', () => {
test('shows loading indicator', () => {
const wrapper = mountComponent(LegalNoticeView);
const legalNoticeLoading = wrapper.find('.legal-notice-loading');
const legalNoticeError = wrapper.find('.error');
const legalNotice = wrapper.find('.legal-notice');
expect(legalNoticeLoading.exists()).toBe(true);
expect(legalNoticeError.exists()).toBe(false);
expect(legalNotice.exists()).toBe(false);
});
test('shows error', async () => {
getLegalNotice.mockRejectedValue(new FetchError());
const wrapper = mountComponent(LegalNoticeView);
await flushPromises();
await wrapper.vm.$nextTick();
const legalNoticeLoading = wrapper.find('.legal-notice-loading');
const legalNoticeError = wrapper.find('.error');
const legalNotice = wrapper.find('.legal-notice');
expect(legalNoticeLoading.exists()).toBe(false);
expect(legalNoticeError.exists()).toBe(true);
expect(legalNotice.exists()).toBe(false);
});
test('shows legal notice', async () => {
const wrapper = mountComponent(LegalNoticeView);
await flushPromises();
await wrapper.vm.$nextTick();
const legalNoticeLoading = wrapper.find('.legal-notice-loading');
const legalNoticeError = wrapper.find('.error');
const legalNotice = wrapper.find('.legal-notice');
expect(legalNoticeLoading.exists()).toBe(false);
expect(legalNoticeError.exists()).toBe(false);
expect(legalNotice.exists()).toBe(true);
});
});
File diff suppressed because it is too large Load Diff
@@ -73,27 +73,12 @@ export const validateEntry = function validateEntry (entry, path) {
throw new Error(`[settings] "${path}.default" must be a string`); throw new Error(`[settings] "${path}.default" must be a string`);
} }
if (entry.type === 'selection') { if (entry.type === 'selection') {
if (entry.allowMultiple) { if (typeof entry.default !== 'string') {
if (!Array.isArray(entry.default)) { throw new Error(`[settings] "${path}.default" must be a string`);
throw new Error(`[settings] "${path}.default" must be an array`); };
} if (!entry.options.map((option) => option.name).includes(entry.default)) {
const allOptions = entry.options.map((option) => option.name); throw new Error(`[settings] option "${path}.default" does not exist`);
entry.default.forEach((defaultValue, index) => { };
if (typeof defaultValue !== 'string') {
throw new Error(`[settings] "${path}.default[${index}]" must be a string`);
}
if (!allOptions.includes(defaultValue)) {
throw new Error(`[settings] option "${path}.default[${index}]" does not exist`);
}
});
} else {
if (typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`);
};
if (!entry.options.map((option) => option.name).includes(entry.default)) {
throw new Error(`[settings] option "${path}.default" does not exist`);
};
}
}; };
} }
} }
+1 -6
View File
@@ -61,10 +61,6 @@ onMounted(async () => {
}); });
settingsLoaded.value = true; settingsLoaded.value = true;
}); });
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSidebarFunction) {
if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction();
};
</script> </script>
<template> <template>
@@ -75,14 +71,13 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi
</h1> </h1>
</header> </header>
<LeftSidebarLayout class="layout"> <LeftSidebarLayout class="layout">
<template #sidebar="{ toggleSidebar }"> <template #sidebar>
<ul class="sidebar-sections-list"> <ul class="sidebar-sections-list">
<li v-for="section in settings"> <li v-for="section in settings">
<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 === section.name }" :class="{ active: activeSection === section.name }"
@click="toggleSidebarIfSmallScreen(toggleSidebar)"
> >
{{ t(section.i18n) }} {{ t(section.i18n) }}
</RouterLink> </RouterLink>
+2 -51
View File
@@ -14,64 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<script setup>
import SidebarExpandButton from './SidebarExpandButton.vue';
const props = defineProps({
'expanded': {
default: false,
required: false,
type: Boolean
}
});
const emit = defineEmits(['toggleExpanded']);
const toggleSidebar = function toggleSidebar () {
emit('toggleExpanded');
};
</script>
<template> <template>
<nav class="sidebar" :class="{ expanded }"> <nav class="sidebar">
<div class="sidebar-controls"> <slot />
<SidebarExpandButton @click="toggleSidebar" class="sidebar-expand-button" />
</div>
<div class="sidebar-content" v-if="props.expanded">
<slot />
</div>
</nav> </nav>
</template> </template>
<style scoped> <style scoped>
.sidebar { .sidebar {
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
}
.sidebar-controls {
display: flex;
align-items: center;
justify-content: end;
padding: 1em 20px;
padding-bottom: 0;
margin-bottom: 8px;
}
.sidebar:not(.expanded) .sidebar-controls {
padding: 1em;
}
@media (max-width: 48rem) {
.sidebar {
border: none;
}
.sidebar:not(.expanded) .sidebar-expand-button {
display: none;
}
}
.sidebar-content {
padding: 20px; padding: 20px;
padding-top: 0;
} }
</style> </style>
@@ -1,41 +0,0 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
const emit = defineEmits(['click']);
const click = function click (event) {
emit('click', event);
};
</script>
<template>
<button class="button sidebar-expand-button">
<Icon name="sidebar-expand" @click="click" />
</button>
</template>
<style scoped>
.sidebar-expand-button {
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
border-radius: 10px;
}
</style>
+5 -66
View File
@@ -16,34 +16,15 @@ limitations under the License.
<script setup> <script setup>
import Sidebar from '@/features/sidebar/components/Sidebar.vue'; import Sidebar from '@/features/sidebar/components/Sidebar.vue';
import SidebarExpandButton from '@/features/sidebar/components/SidebarExpandButton.vue';
import { ref } from 'vue';
const props = defineProps({
'expanded': {
default: false,
required: false,
type: Boolean
}
});
const expanded = ref(props.expanded);
const toggleSidebar = function toggleSidebar () {
expanded.value = !expanded.value;
};
</script> </script>
<template> <template>
<div class="layout-container" :class="{ expanded }"> <div class="layout-container">
<div class="sidebar-expand-button-container"> <Sidebar>
<SidebarExpandButton class="sidebar-expand-button" @click="toggleSidebar" /> <slot name="sidebar" />
</div>
<Sidebar @toggle-expanded="toggleSidebar" :expanded="expanded" class="sidebar">
<slot name="sidebar" :toggleSidebar="toggleSidebar" />
</Sidebar> </Sidebar>
<main class="main-content"> <main class="main-content">
<slot :toggleSidebar="toggleSidebar" /> <slot />
</main> </main>
</div> </div>
</template> </template>
@@ -51,53 +32,11 @@ const toggleSidebar = function toggleSidebar () {
<style scoped> <style scoped>
.layout-container { .layout-container {
display: grid; display: grid;
grid-template-columns: calc(32px + 2em) 1fr; grid-template-columns: min(24%, 280px) 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"sidebar-expand sidebar-expand"
"sidebar main-content";
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.layout-container.expanded {
grid-template-columns: min(24%, 280px) 1fr;
}
.sidebar-expand-button-container {
grid-area: sidebar-expand;
display: none;
}
.sidebar {
grid-area: sidebar;
}
@media (max-width: 48rem) {
.layout-container {
grid-template-columns: 0 1fr;
}
.layout-container.expanded {
grid-template-columns: 1fr 0;
}
.layout-container:not(.expanded) .sidebar-expand-button-container {
justify-content: end;
display: block;
padding: 20px;
}
.layout-container:not(.expanded) .main-content {
padding-top: 0;
}
.layout-container.expanded .main-content {
display: none;
}
}
.main-content { .main-content {
grid-area: main-content;
padding: 20px; padding: 20px;
} }
</style> </style>
-6
View File
@@ -61,11 +61,5 @@
"ariaLabel": "Wählt die Option {option} aus" "ariaLabel": "Wählt die Option {option} aus"
} }
} }
},
"legal": {
"notice": {
"title": "Impressum",
"loadError": "Das Impressum konnte nicht geladen werden."
}
} }
} }
-6
View File
@@ -61,11 +61,5 @@
"ariaLabel": "Selects option {option}" "ariaLabel": "Selects option {option}"
} }
} }
},
"legal": {
"notice": {
"title": "Legal Notice",
"loadError": "The legal notice could not be loaded."
}
} }
} }
+1 -24
View File
@@ -38,28 +38,5 @@
} }
} }
}, },
"slogan": "Hecho para buscar.", "slogan": "Hecho para buscar."
"settings": {
"input": {
"done": {
"title": "Guardar valor",
"ariaLabel": "Guarda el valor introducido"
}
},
"switch": {
"title": "Cambiar valor",
"ariaLabel": "Cambia el valor"
},
"pages": {
"link": {
"ariaLabel": "Va a la página de configuración {page}"
}
},
"noSectionSelected": "No se ha seleccionado ninguna sección de configuración.",
"selection": {
"option": {
"ariaLabel": "Selecciona la opción {option}"
}
}
}
} }
+1 -24
View File
@@ -38,28 +38,5 @@
} }
} }
}, },
"slogan": "Conçu pour chercher.", "slogan": "Conçu pour chercher."
"settings": {
"input": {
"done": {
"title": "Enregistrer la valeur",
"ariaLabel": "Enregistre la valeur saisie"
}
},
"switch": {
"title": "Basculer la valeur",
"ariaLabel": "Bascule la valeur"
},
"pages": {
"link": {
"ariaLabel": "Accéder à la page de paramètres {page}"
}
},
"noSectionSelected": "Aucune section des paramètres n'est sélectionnée.",
"selection": {
"option": {
"ariaLabel": "Sélectionne l'option {option}"
}
}
}
} }
+1 -24
View File
@@ -38,28 +38,5 @@
} }
} }
}, },
"slogan": "Costruito per cercare.", "slogan": "Costruito per cercare."
"settings": {
"input": {
"done": {
"title": "Salva valore",
"ariaLabel": "Salva il valore inserito"
}
},
"switch": {
"title": "Attiva/disattiva valore",
"ariaLabel": "Attiva o disattiva il valore"
},
"pages": {
"link": {
"ariaLabel": "Vai alla pagina delle impostazioni {page}"
}
},
"noSectionSelected": "Nessuna sezione delle impostazioni è selezionata.",
"selection": {
"option": {
"ariaLabel": "Seleziona l'opzione {option}"
}
}
}
} }
+1 -24
View File
@@ -38,28 +38,5 @@
} }
} }
}, },
"slogan": "Feito para pesquisar.", "slogan": "Feito para pesquisar."
"settings": {
"input": {
"done": {
"title": "Guardar valor",
"ariaLabel": "Guarda o valor introduzido"
}
},
"switch": {
"title": "Alternar valor",
"ariaLabel": "Alterna o valor"
},
"pages": {
"link": {
"ariaLabel": "Ir para a página de definições {page}"
}
},
"noSectionSelected": "Nenhuma secção de definições está selecionada.",
"selection": {
"option": {
"ariaLabel": "Seleciona a opção {option}"
}
}
}
} }
-20
View File
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { getAssetsUrl } from './config/env';
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
@@ -26,29 +25,10 @@ import './styles/common.css'
import './styles/variables/colors.css' 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/common.css';
import './styles/variables/colors.css'; import './styles/variables/colors.css';
(async () => { (async () => {
const fonts = rawFonts.map((font) => new FontFace(
font.family ?? '',
Array.isArray(font.source)
? font.source.map(
(source) => `url('${source.url
? new URL(source.url, getAssetsUrl()).href
: ''
}') format('${source.format ?? ''}')`
).join(',')
: '',
{
weight: font.weight ?? undefined,
style: font.style ?? undefined
}
));
await Promise.all(fonts.map((font) => font.load()));
fonts.forEach((font) => document.fonts.add(font));
await loadLanguage(fallbackLocale); await loadLanguage(fallbackLocale);
await loadLanguage(getCurrentLanguage()); await loadLanguage(getCurrentLanguage());
-9
View File
@@ -18,7 +18,6 @@ import { createRouter, createWebHistory } from 'vue-router';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import SearchView from '../views/SearchView.vue'; import SearchView from '../views/SearchView.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 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';
@@ -40,14 +39,6 @@ const routes = [
title: (route) => route.query.q title: (route) => route.query.q
} }
}, },
{
path: '/legal/notice',
name: 'legalNotice',
component: LegalNoticeView,
meta: {
title: () => i18n.global.t('legal.notice.title')
}
},
{ {
path: '/settings/:rest(.*)*', path: '/settings/:rest(.*)*',
name: 'settings', name: 'settings',
+2 -17
View File
@@ -19,15 +19,13 @@ 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;
} }
.link { .link {
text-decoration: none; text-decoration: none;
color: var(--dark);
} }
.link:hover:not(.button-link), .link:focus-visible:not(.button-link) { .link:hover:not(.button-link), .link:focus:not(.button-link) {
outline: none; outline: none;
text-decoration: underline; text-decoration: underline;
} }
@@ -80,7 +78,7 @@ input {
color: inherit; color: inherit;
} }
.button:focus-visible { .button:focus {
background-color: var(--light-hover); background-color: var(--light-hover);
outline: none; outline: none;
} }
@@ -88,16 +86,3 @@ input {
.button:hover { .button:hover {
background-color: var(--light-hover); background-color: var(--light-hover);
} }
/* Temporary until a custom tap highlight is implemented. */
* {
-webkit-tap-highlight-color: transparent;
}
.error {
background-color: var(--color-negative-bg);
border: 1px solid var(--color-negative-border);
padding: 1em 2em;
border-radius: 1.2em;
color: var(--white);
}
-32
View File
@@ -1,32 +0,0 @@
[
{
"family": "Inter",
"weight": "100 900",
"style": "normal",
"source": [
{
"url": "fonts/Inter/Inter-Variable.woff2",
"format": "woff2"
},
{
"url": "fonts/Inter/Inter-Variable.ttf",
"format": "truetype"
}
]
},
{
"family": "Inter",
"weight": "100 900",
"style": "italic",
"source": [
{
"url": "fonts/Inter/Inter-Italic-Variable.woff2",
"format": "woff2"
},
{
"url": "fonts/Inter/Inter-Italic-Variable.ttf",
"format": "truetype"
}
]
}
]
-3
View File
@@ -84,9 +84,6 @@ body {
--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-border: oklch(51% 0.22706 29.244);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
+89 -91
View File
@@ -14,111 +14,109 @@ 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 { expect, test } from 'vitest';
import { ensureUnit } from '../cssDimensions'; import { ensureUnit } from '../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 )', expected: 'clamp( 42vh,23vw,13cap )' } { dimension: 'clamp( 42vh,23vw,13cap )', expected: 'clamp( 42vh,23vw,13cap )' }
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => { ])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
expect(ensureUnit(dimension)).toBe(expected); expect(ensureUnit(dimension)).toBe(expected);
});
}); });
+35 -35
View File
@@ -14,48 +14,48 @@ 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 { expect, test } from 'vitest';
import getCurrentLanguage from '../currentLanguage'; import getCurrentLanguage from '../currentLanguage';
describe('currentLanguage', () => { const locales = [
test.for([ { navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'}, { navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'}, { navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'}, { navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'}, { navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'}, { navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'}, { navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'}, { navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'}, { navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'}, { navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'}, { navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'}, { navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'}, { navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'}, { navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'}, { navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'}, { navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'}, { navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'}, { navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'}, { navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'}, { navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'}, { navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'} { navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'}
])('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => { ];
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
if (localStorageLanguage) { test.for(locales)('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => {
localStorage.setItem('locale', localStorageLanguage); Object.defineProperty(navigator, 'language', {
}; value: navigatorLanguage,
configurable: true
expect(getCurrentLanguage()).toBe(expected);
}); });
if (localStorageLanguage) {
localStorage.setItem('locale', localStorageLanguage);
};
expect(getCurrentLanguage()).toBe(expected);
}); });