generated from Seekra/repository-template
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a242bbad18
|
||
|
|
d28eb0a9d3
|
||
|
|
d1e95582a8
|
||
|
|
e5f27041d2
|
||
|
|
baa924a2b2
|
||
|
|
26535cad45
|
||
|
|
5fdf0490b6
|
||
|
|
61817bf4fc
|
||
|
|
0f71f1240e
|
||
|
|
d67c8aa80e
|
||
|
|
44c457ba20
|
||
|
|
4b010d03ac
|
||
|
|
813cc7c7fc
|
||
|
|
a77bbec3fa
|
||
|
|
447207e5f0
|
@@ -0,0 +1,16 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useSettingsStore } from '../stores/settingsStore';
|
import { useSettingsStore } from '../stores/settingsStore';
|
||||||
import { useId } from 'vue';
|
import { ref, useId, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -35,8 +35,16 @@ const switchId = useId();
|
|||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
|
|
||||||
|
const enabled = ref(null);
|
||||||
|
watch(store, (newStore) => {
|
||||||
|
enabled.value = newStore.get(props.path);
|
||||||
|
});
|
||||||
|
// set value after registering watcher to avoid immediate value change
|
||||||
|
enabled.value = store.get(props.path) ?? props.setting.default;
|
||||||
|
|
||||||
const toggle = function toggle () {
|
const toggle = function toggle () {
|
||||||
store.set(props.path, !store.get(props.path));
|
enabled.value = !enabled.value;
|
||||||
|
store.set(props.path, enabled.value);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -47,7 +55,7 @@ const toggle = function toggle () {
|
|||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="switch-wrapper"
|
class="switch-wrapper"
|
||||||
:class="{ enabled: store.get(props.path) }"
|
:class="{ enabled }"
|
||||||
:title="t('settings.switch.title')"
|
:title="t('settings.switch.title')"
|
||||||
:aria-label="t('settings.switch.ariaLabel')"
|
:aria-label="t('settings.switch.ariaLabel')"
|
||||||
:aria-labelledby="labelId"
|
:aria-labelledby="labelId"
|
||||||
|
|||||||
@@ -20,17 +20,23 @@ import { useSettingsStore } from '../../stores/settingsStore.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 ({ i18n = 'switch1', translations = {} } = {}) {
|
const getWrapper = function getWrapper ({
|
||||||
|
i18n = 'switch1',
|
||||||
|
defaultValue = undefined,
|
||||||
|
translations = {},
|
||||||
|
piniaOptions = {}
|
||||||
|
} = {}) {
|
||||||
return mountComponent(Switch, {
|
return mountComponent(Switch, {
|
||||||
attrs: {
|
attrs: {
|
||||||
setting: {
|
setting: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
name: 'switch',
|
name: 'switch',
|
||||||
i18n
|
i18n,
|
||||||
|
default: defaultValue
|
||||||
},
|
},
|
||||||
path: 'switch'
|
path: 'switch'
|
||||||
}
|
}
|
||||||
}, translations);
|
}, translations, [], piniaOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Switch', () => {
|
describe('Switch', () => {
|
||||||
@@ -53,6 +59,42 @@ describe('Switch', () => {
|
|||||||
expect(switchElementClasses2).not.toContain('enabled');
|
expect(switchElementClasses2).not.toContain('enabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows default value', () => {
|
||||||
|
const wrapper = getWrapper({
|
||||||
|
defaultValue: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const switchElement = wrapper.find('.switch-wrapper');
|
||||||
|
expect(switchElement.classes('enabled')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows stored value instead of default value', () => {
|
||||||
|
const wrapper = getWrapper({
|
||||||
|
defaultValue: true,
|
||||||
|
piniaOptions: {
|
||||||
|
setupStores: () => {
|
||||||
|
const store = useSettingsStore();
|
||||||
|
store.set('switch', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const switchElement = wrapper.find('.switch-wrapper');
|
||||||
|
expect(switchElement.classes('enabled')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reacts on store change', async () => {
|
||||||
|
const wrapper = getWrapper();
|
||||||
|
const store = useSettingsStore();
|
||||||
|
|
||||||
|
const switchElement = wrapper.find('.switch-wrapper');
|
||||||
|
expect(switchElement.classes('enabled')).toBeFalsy();
|
||||||
|
|
||||||
|
store.set('switch', true);
|
||||||
|
await nextTick();
|
||||||
|
expect(switchElement.classes('enabled')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
test('toggles value in store', async () => {
|
test('toggles value in store', async () => {
|
||||||
const wrapper = getWrapper();
|
const wrapper = getWrapper();
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
settingsLoaded.value = true;
|
settingsLoaded.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSidebarFunction) {
|
||||||
|
if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -71,13 +75,14 @@ onMounted(async () => {
|
|||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<LeftSidebarLayout class="layout">
|
<LeftSidebarLayout class="layout">
|
||||||
<template #sidebar>
|
<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">
|
||||||
<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>
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ const toggleSidebar = function toggleSidebar () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 48rem) {
|
@media (max-width: 48rem) {
|
||||||
|
.sidebar {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar:not(.expanded) .sidebar-expand-button {
|
.sidebar:not(.expanded) .sidebar-expand-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const expanded = ref(props.expanded);
|
const expanded = ref(props.expanded);
|
||||||
|
|
||||||
const toggleExpanded = function toggleExpanded () {
|
const toggleSidebar = function toggleSidebar () {
|
||||||
expanded.value = !expanded.value;
|
expanded.value = !expanded.value;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -37,13 +37,13 @@ const toggleExpanded = function toggleExpanded () {
|
|||||||
<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="toggleExpanded" />
|
<SidebarExpandButton class="sidebar-expand-button" @click="toggleSidebar" />
|
||||||
</div>
|
</div>
|
||||||
<Sidebar @toggle-expanded="toggleExpanded" :expanded="expanded" class="sidebar">
|
<Sidebar @toggle-expanded="toggleSidebar" :expanded="expanded" class="sidebar">
|
||||||
<slot name="sidebar" />
|
<slot name="sidebar" :toggleSidebar="toggleSidebar" />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<slot />
|
<slot :toggleSidebar="toggleSidebar" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+20
@@ -14,6 +14,7 @@ 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';
|
||||||
@@ -25,10 +26,29 @@ 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());
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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 {
|
||||||
@@ -86,3 +87,8 @@ 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user