generated from Seekra/repository-template
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f485a2d476
|
||
|
|
dde56c42ae
|
||
|
|
15697144b1
|
||
|
|
396c025ec4
|
||
|
|
30f6b82416
|
||
|
|
7b09cd74cb
|
||
|
|
fab7d7e901
|
||
|
|
04c148a3b1
|
||
|
|
ff6ab7a78c
|
||
|
|
294fdab550
|
||
|
|
26eec92f42
|
||
|
|
a01abe67e1
|
||
|
|
3eaf81a465
|
||
|
|
2addd779a7
|
||
|
|
6afbf3efed
|
||
|
|
dca7f2176b
|
||
|
|
60d3296816
|
||
|
|
b0642cc826
|
||
|
|
32555d9acb
|
||
|
|
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
|
||||
+2
-1
@@ -26,7 +26,7 @@ import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
|
||||
const { getColorScheme, updateColorScheme } = useColorScheme();
|
||||
const colorScheme = ref(getColorScheme());
|
||||
const colorScheme = ref(null);
|
||||
provide('colorScheme', colorScheme);
|
||||
watch(colorScheme, (newValue) => {
|
||||
updateColorScheme(newValue);
|
||||
@@ -46,6 +46,7 @@ watch(colorScheme, (newValue) => {
|
||||
document.body.classList.remove('color-scheme-auto');
|
||||
}
|
||||
});
|
||||
colorScheme.value = getColorScheme();
|
||||
|
||||
watchEffect(() => updatePageTitle(route));
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -54,6 +54,7 @@ const copyrightPeriod =
|
||||
.footer-segment {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
padding: var(--padding-y);
|
||||
background-color: var(--light-bg);
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
<script setup>
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
import { useId } from 'vue';
|
||||
import { ref, useId, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -35,8 +35,16 @@ const switchId = useId();
|
||||
|
||||
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 () {
|
||||
store.set(props.path, !store.get(props.path));
|
||||
enabled.value = !enabled.value;
|
||||
store.set(props.path, enabled.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -47,7 +55,7 @@ const toggle = function toggle () {
|
||||
</label>
|
||||
<div
|
||||
class="switch-wrapper"
|
||||
:class="{ enabled: store.get(props.path) }"
|
||||
:class="{ enabled }"
|
||||
:title="t('settings.switch.title')"
|
||||
:aria-label="t('settings.switch.ariaLabel')"
|
||||
:aria-labelledby="labelId"
|
||||
|
||||
@@ -20,17 +20,23 @@ import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import { expect, describe, test } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const getWrapper = function getWrapper ({ i18n = 'switch1', translations = {} } = {}) {
|
||||
const getWrapper = function getWrapper ({
|
||||
i18n = 'switch1',
|
||||
defaultValue = undefined,
|
||||
translations = {},
|
||||
piniaOptions = {}
|
||||
} = {}) {
|
||||
return mountComponent(Switch, {
|
||||
attrs: {
|
||||
setting: {
|
||||
type: 'bool',
|
||||
name: 'switch',
|
||||
i18n
|
||||
i18n,
|
||||
default: defaultValue
|
||||
},
|
||||
path: 'switch'
|
||||
}
|
||||
}, translations);
|
||||
}, translations, [], piniaOptions);
|
||||
};
|
||||
|
||||
describe('Switch', () => {
|
||||
@@ -53,6 +59,42 @@ describe('Switch', () => {
|
||||
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 () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 { useSettingsConfigCacheStore } from '../../stores/settingsConfigCacheStore';
|
||||
import { useSettingsStore } from '../../stores/settingsStore';
|
||||
import { useSettings } from '../useSettings';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const settingDefaultValue = 42;
|
||||
const settingPath = ['test', 'number'];
|
||||
|
||||
describe('useSettings', () => {
|
||||
describe('getSetting', () => {
|
||||
test('returns stored value', () => {
|
||||
const { getSetting } = useSettings();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
const key = 'setting';
|
||||
const value = 42;
|
||||
|
||||
settings.set(key, value);
|
||||
|
||||
expect(getSetting(key)).toBe(value);
|
||||
});
|
||||
|
||||
test('returns default value', () => {
|
||||
const { getSetting } = useSettings();
|
||||
expect(getSetting(settingPath)).toBe(settingDefaultValue);
|
||||
});
|
||||
|
||||
test('returns stored value instead of default value', () => {
|
||||
const { getSetting } = useSettings();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
const value = 43;
|
||||
|
||||
expect(getSetting(settingPath)).toBe(settingDefaultValue);
|
||||
|
||||
settings.set(settingPath, value);
|
||||
expect(getSetting(settingPath)).toBe(value);
|
||||
});
|
||||
|
||||
test('returns undefined if there is no value', () => {
|
||||
const { getSetting } = useSettings();
|
||||
expect(getSetting(['test', 'bool'])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false
|
||||
})
|
||||
);
|
||||
const settingsConfigCache = useSettingsConfigCacheStore();
|
||||
settingsConfigCache.set({
|
||||
contents: [
|
||||
{
|
||||
name: 'test',
|
||||
i18n: '',
|
||||
content: [
|
||||
{
|
||||
type: 'number',
|
||||
i18n: '',
|
||||
name: 'number',
|
||||
default: settingDefaultValue
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
i18n: '',
|
||||
name: 'bool'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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 { useSettingsConfigCacheStore } from '../stores/settingsConfigCacheStore';
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
import { getSettingRecursively } from '../utils/getSetting';
|
||||
import { loadSettingsConfig } from '../utils/settingsParser';
|
||||
|
||||
/**
|
||||
* Provides access to the stored settings.
|
||||
*/
|
||||
export const useSettings = function useSettings () {
|
||||
/**
|
||||
* Returns the value of a specific setting.
|
||||
* @param {string[]} key - The settings key.
|
||||
* @return The value of the setting.
|
||||
*/
|
||||
const getSetting = function getSetting (key) {
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const settings = useSettingsConfigCacheStore().get();
|
||||
return (
|
||||
settingsStore.get(key) ??
|
||||
getSettingRecursively(
|
||||
key, settings.contents ?? []
|
||||
)?.default
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the value of a specific setting.
|
||||
* @param {string[]} key - The setting key.
|
||||
* @param value - The new value for the setting.
|
||||
*/
|
||||
const setSetting = function setSetting (key, value) {
|
||||
const settingsStore = useSettingsStore();
|
||||
settingsStore.set(key.join('.'), value);
|
||||
};
|
||||
|
||||
return { getSetting, setSetting };
|
||||
};
|
||||
@@ -1,3 +1,30 @@
|
||||
{
|
||||
"contents": []
|
||||
}
|
||||
"contents": [
|
||||
{
|
||||
"name": "appearance",
|
||||
"i18n": "settings.settings.appearance.title",
|
||||
"content": [
|
||||
{
|
||||
"name": "colorScheme",
|
||||
"type": "selection",
|
||||
"i18n": "settings.settings.appearance.contents.colorScheme.title",
|
||||
"default": "auto",
|
||||
"options": [
|
||||
{
|
||||
"name": "auto",
|
||||
"i18n": "settings.settings.appearance.contents.colorScheme.options.auto"
|
||||
},
|
||||
{
|
||||
"name": "light",
|
||||
"i18n": "settings.settings.appearance.contents.colorScheme.options.light"
|
||||
},
|
||||
{
|
||||
"name": "dark",
|
||||
"i18n": "settings.settings.appearance.contents.colorScheme.options.dark"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 { defineStore } from 'pinia';
|
||||
|
||||
export const useSettingsConfigCacheStore = defineStore('settingsConfigCache', () => {
|
||||
let settingsConfig = undefined;
|
||||
|
||||
const set = function set (value) {
|
||||
settingsConfig = value;
|
||||
};
|
||||
|
||||
const get = function get () {
|
||||
return settingsConfig;
|
||||
};
|
||||
|
||||
return { set, get };
|
||||
});
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useSettingsConfigCacheStore } from '../stores/settingsConfigCacheStore';
|
||||
import { validateSettingsConfig } from './settingsValidator.js';
|
||||
|
||||
/**
|
||||
@@ -21,10 +22,12 @@ import { validateSettingsConfig } from './settingsValidator.js';
|
||||
* @returns {Promise<import('../types/settingsConfig').SettingsConfig>}
|
||||
*/
|
||||
export async function loadSettingsConfig() {
|
||||
const settingsConfigCache = useSettingsConfigCacheStore();
|
||||
let raw;
|
||||
|
||||
try {
|
||||
raw = (await import('../settings.json')).default;
|
||||
settingsConfigCache.set(raw);
|
||||
} catch (e) {
|
||||
throw new Error(`[settings] Failed to load settings.json: ${e.message}`);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ onMounted(async () => {
|
||||
});
|
||||
settingsLoaded.value = true;
|
||||
});
|
||||
|
||||
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSidebarFunction) {
|
||||
if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -71,13 +75,14 @@ onMounted(async () => {
|
||||
</h1>
|
||||
</header>
|
||||
<LeftSidebarLayout class="layout">
|
||||
<template #sidebar>
|
||||
<template #sidebar="{ toggleSidebar }">
|
||||
<ul class="sidebar-sections-list">
|
||||
<li v-for="section in settings">
|
||||
<RouterLink
|
||||
:to="`/settings/${section.name}`"
|
||||
class="button button-link link sidebar-section"
|
||||
:class="{ active: activeSection === section.name }"
|
||||
@click="toggleSidebarIfSmallScreen(toggleSidebar)"
|
||||
>
|
||||
{{ t(section.i18n) }}
|
||||
</RouterLink>
|
||||
|
||||
@@ -36,7 +36,7 @@ const toggleSidebar = function toggleSidebar () {
|
||||
<div class="sidebar-controls">
|
||||
<SidebarExpandButton @click="toggleSidebar" class="sidebar-expand-button" />
|
||||
</div>
|
||||
<div class="sidebar-content" v-if="props.expanded">
|
||||
<div class="sidebar-content" v-show="props.expanded">
|
||||
<slot />
|
||||
</div>
|
||||
</nav>
|
||||
@@ -61,6 +61,10 @@ const toggleSidebar = function toggleSidebar () {
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sidebar {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.sidebar:not(.expanded) .sidebar-expand-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const props = defineProps({
|
||||
|
||||
const expanded = ref(props.expanded);
|
||||
|
||||
const toggleExpanded = function toggleExpanded () {
|
||||
const toggleSidebar = function toggleSidebar () {
|
||||
expanded.value = !expanded.value;
|
||||
};
|
||||
</script>
|
||||
@@ -37,13 +37,13 @@ const toggleExpanded = function toggleExpanded () {
|
||||
<template>
|
||||
<div class="layout-container" :class="{ expanded }">
|
||||
<div class="sidebar-expand-button-container">
|
||||
<SidebarExpandButton class="sidebar-expand-button" @click="toggleExpanded" />
|
||||
<SidebarExpandButton class="sidebar-expand-button" @click="toggleSidebar" />
|
||||
</div>
|
||||
<Sidebar @toggle-expanded="toggleExpanded" :expanded="expanded" class="sidebar">
|
||||
<slot name="sidebar" />
|
||||
<Sidebar @toggle-expanded="toggleSidebar" :expanded="expanded" class="sidebar">
|
||||
<slot name="sidebar" :toggleSidebar="toggleSidebar" />
|
||||
</Sidebar>
|
||||
<main class="main-content">
|
||||
<slot />
|
||||
<slot :toggleSidebar="toggleSidebar" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,6 +60,21 @@
|
||||
"option": {
|
||||
"ariaLabel": "Wählt die Option {option} aus"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Erscheinungsbild",
|
||||
"contents": {
|
||||
"colorScheme": {
|
||||
"title": "Farbschema",
|
||||
"options": {
|
||||
"auto": "An Systemfarbschema anpassen",
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,21 @@
|
||||
"option": {
|
||||
"ariaLabel": "Selects option {option}"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"contents": {
|
||||
"colorScheme": {
|
||||
"title": "Color Scheme",
|
||||
"options": {
|
||||
"auto": "Adapt to system color scheme",
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getAssetsUrl } from './config/env';
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
@@ -25,10 +26,29 @@ import './styles/common.css'
|
||||
import './styles/variables/colors.css'
|
||||
import './styles/variables/dimensions.css';
|
||||
|
||||
import rawFonts from './styles/fonts.json';
|
||||
import './styles/common.css';
|
||||
import './styles/variables/colors.css';
|
||||
|
||||
(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(getCurrentLanguage());
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ body {
|
||||
color: var(--dark);
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.link {
|
||||
@@ -85,4 +86,9 @@ input {
|
||||
|
||||
.button: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