mirror of
https://iceshrimp.dev/Crimekillz/jointrashposs.git
synced 2024-11-25 10:19:07 +01:00
(add) サーバー統計ページ (#58)
* (add) サーバー統計ページ * fix * add jsdoc * Update CircGraph.vue
This commit is contained in:
parent
76dc26237a
commit
4c47eb20ee
7 changed files with 405 additions and 1 deletions
123
components/charts/CircGraph.vue
Normal file
123
components/charts/CircGraph.vue
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<svg viewBox="-3 -3 70 70">
|
||||||
|
<circle
|
||||||
|
v-for="(item, index) in sortedData"
|
||||||
|
:class="[
|
||||||
|
$style.pie,
|
||||||
|
(focusedIndex === index) && $style.focused,
|
||||||
|
]"
|
||||||
|
:style="getStyle(index)"
|
||||||
|
:data-real-value="item"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 軽量円グラフ描画コンポーネント(すごい)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
/** グラフにしたいデータ(数字の配列 or kv形式のオブジェクト) */
|
||||||
|
data: number[] | Record<string | number | symbol, number>;
|
||||||
|
/** 多い順にソートするか?(デフォルトON) */
|
||||||
|
sort?: boolean;
|
||||||
|
/** 小さな値を「その他」としてまとめるか?(デフォルトON、数値指定でその割合以下の項目をまとめる) */
|
||||||
|
truncMinor?: boolean | number;
|
||||||
|
/** グラデーション開始色([r, g, b]の配列で指定) */
|
||||||
|
startColor?: [number, number, number];
|
||||||
|
/** グラデーション終了色([r, g, b]の配列で指定) */
|
||||||
|
endColor?: [number, number, number];
|
||||||
|
/** 強調するindex番号を指定 */
|
||||||
|
focusedIndex?: number;
|
||||||
|
}>(), {
|
||||||
|
sort: true,
|
||||||
|
truncMinor: true,
|
||||||
|
startColor: [74, 179, 0],
|
||||||
|
endColor: [230, 255, 148],
|
||||||
|
});
|
||||||
|
|
||||||
|
const isReady = ref(false);
|
||||||
|
|
||||||
|
const sortedData = computed(() => {
|
||||||
|
let out = Array.isArray(props.data) ? props.data : Object.values(props.data);
|
||||||
|
const sum = out.reduce((p, c) => p + c, 0);
|
||||||
|
|
||||||
|
if (props.sort) {
|
||||||
|
out = out.sort((a, b) => b - a);
|
||||||
|
}
|
||||||
|
if (props.truncMinor !== false) {
|
||||||
|
const ratio = props.truncMinor === true ? 0.02 : props.truncMinor;
|
||||||
|
const toBeTrunked = out.filter((v) => v / sum < ratio);
|
||||||
|
out = [
|
||||||
|
...out.filter((v) => v / sum >= ratio),
|
||||||
|
toBeTrunked.reduce((p, c) => p + c, 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
|
||||||
|
const steppedColors = computed(() => {
|
||||||
|
const r = sortedData.value.map((_, i, a) => props.startColor[0] - (props.startColor[0] - props.endColor[0]) / a.length * i);
|
||||||
|
const g = sortedData.value.map((_, i, a) => props.startColor[1] - (props.startColor[1] - props.endColor[1]) / a.length * i);
|
||||||
|
const b = sortedData.value.map((_, i, a) => props.startColor[2] - (props.startColor[2] - props.endColor[2]) / a.length * i);
|
||||||
|
return sortedData.value.map((_, i) => [r[i], g[i], b[i]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dasharrays = computed(() => {
|
||||||
|
const sum = sortedData.value.reduce((p, c) => p + c, 0);
|
||||||
|
|
||||||
|
const out: number[][] = [];
|
||||||
|
let start = 0;
|
||||||
|
for (let index = 0; index < sortedData.value.length; index++) {
|
||||||
|
const element = sortedData.value[index];
|
||||||
|
const percentage = element / sum;
|
||||||
|
if (index === 0) {
|
||||||
|
out[index] = [percentage * 100, 100 - percentage * 100];
|
||||||
|
start = percentage * 100;
|
||||||
|
} else {
|
||||||
|
out[index] = [
|
||||||
|
0,
|
||||||
|
start,
|
||||||
|
percentage * 100,
|
||||||
|
Math.max(100 - (start + (percentage * 100)), 0),
|
||||||
|
];
|
||||||
|
start += percentage * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.map((v) => v.join(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
function getStyle(index: number) {
|
||||||
|
return `stroke: rgb(${steppedColors.value[index][0]}, ${steppedColors.value[index][1]}, ${steppedColors.value[index][2]});
|
||||||
|
--dasharray: ${dasharrays.value[index]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
watch(dasharrays, (to) => {
|
||||||
|
if (to.length > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
isReady.value = true;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.pie {
|
||||||
|
@apply transition-transform;
|
||||||
|
transform-origin: center;
|
||||||
|
fill: transparent;
|
||||||
|
cx: 32;
|
||||||
|
cy: 32;
|
||||||
|
r: 16;
|
||||||
|
stroke-width: 32;
|
||||||
|
stroke-dashoffset: 25;
|
||||||
|
stroke-dasharray: var(--dasharray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused.pie {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
</style>
|
227
components/servers/StatsViewer.vue
Normal file
227
components/servers/StatsViewer.vue
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto max-w-screen-lg px-6 space-y-6 lg:space-y-8">
|
||||||
|
<GLocalNav :items="[
|
||||||
|
{
|
||||||
|
name: $t('_servers._statistics.lang'),
|
||||||
|
anchor: '#lang',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: $t('_servers._statistics.registerAcceptance'),
|
||||||
|
anchor: '#registerAcceptance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: $t('_servers._statistics.notes'),
|
||||||
|
anchor: '#notes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: $t('_servers._statistics.version'),
|
||||||
|
anchor: '#version'
|
||||||
|
}
|
||||||
|
]" />
|
||||||
|
<div id="lang">
|
||||||
|
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.lang`) }}</h2>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="langStats" :focusedIndex="langFocus" /></div>
|
||||||
|
<div>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li v-for="value, key, index in trunc(langStats)" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="langFocus = index" @mouseleave.passive="langFocus = undefined">
|
||||||
|
<div v-if="key === '__others'">{{ $t('other') }}</div>
|
||||||
|
<div v-else>{{ key }} {{ lang.find((v) => v.lang === key)?.label ? `(${lang.find((v) => v.lang === key)?.label})` : '' }}</div>
|
||||||
|
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="registerAcceptance">
|
||||||
|
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.registerAcceptance`) }}</h2>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="regAcceptanceStats" :focusedIndex="regAcceptanceFocus" :truncMinor="false" /></div>
|
||||||
|
<div>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li v-for="value, key, index in regAcceptanceStats" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="regAcceptanceFocus = index" @mouseleave.passive="regAcceptanceFocus = undefined">
|
||||||
|
<div>{{ $t(`_servers._registerAcceptance.${key}`) }}</div>
|
||||||
|
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="notes">
|
||||||
|
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.notes`) }}</h2>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="notesCountStats" :focusedIndex="notesCountFocus" /></div>
|
||||||
|
<div>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li v-for="value, key, index in trunc(notesCountStats)" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="notesCountFocus = index" @mouseleave.passive="notesCountFocus = undefined">
|
||||||
|
<div v-if="key === '__others'">{{ $t('other') }}</div>
|
||||||
|
<div v-else>{{ key }}</div>
|
||||||
|
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="version">
|
||||||
|
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.version`) }}</h2>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="avgVersionStats" :truncMinor="0.005" :focusedIndex="avgVersionFocus" /></div>
|
||||||
|
<div>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li v-for="value, key, index in trunc(avgVersionStats, 0.005)" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="avgVersionFocus = index" @mouseleave.passive="avgVersionFocus = undefined">
|
||||||
|
<div v-if="key === '__others'">{{ $t('other') }}</div>
|
||||||
|
<div v-else>{{ key }}</div>
|
||||||
|
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { InstanceInfo } from '@/types/instances-info';
|
||||||
|
import lang from '~/assets/data/lang';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const { data } = await useFetch<InstanceInfo>('https://instanceapp.misskey.page/instances.json', {
|
||||||
|
onRequestError: () => {
|
||||||
|
alert(t('_servers._system.fetchError'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const langStats = computed(() => {
|
||||||
|
const d = data.value?.instancesInfos;
|
||||||
|
if (!d || d.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out: Record<string, number> = {};
|
||||||
|
d.forEach((v) => {
|
||||||
|
if (!out[v.langs[0]]) {
|
||||||
|
out[v.langs[0]] = 1;
|
||||||
|
} else {
|
||||||
|
out[v.langs[0]]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(out)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.reduce(
|
||||||
|
(r, [k, v]) => ({
|
||||||
|
...r,
|
||||||
|
[k]: v
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const langFocus = ref<number | undefined>();
|
||||||
|
|
||||||
|
const regAcceptanceStats = computed(() => {
|
||||||
|
const d = data.value?.instancesInfos;
|
||||||
|
if (!d || d.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = {
|
||||||
|
public: 0,
|
||||||
|
inviteOnly: 0,
|
||||||
|
};
|
||||||
|
d.forEach((v) => {
|
||||||
|
if (v.meta?.disableRegistration) {
|
||||||
|
out.inviteOnly++;
|
||||||
|
} else {
|
||||||
|
out.public++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(out)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.reduce(
|
||||||
|
(r, [k, v]) => ({
|
||||||
|
...r,
|
||||||
|
[k]: v
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const regAcceptanceFocus = ref<number | undefined>();
|
||||||
|
|
||||||
|
const notesCountStats = computed(() => {
|
||||||
|
const d = data.value?.instancesInfos;
|
||||||
|
if (!d || d.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out: Record<string, number> = {};
|
||||||
|
|
||||||
|
d.forEach((v) => {
|
||||||
|
out[v.name] = v.stats?.originalNotesCount ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(out)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.reduce(
|
||||||
|
(r, [k, v]) => ({
|
||||||
|
...r,
|
||||||
|
[k]: v
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const notesCountFocus = ref<number | undefined>();
|
||||||
|
|
||||||
|
const avgVersionStats = computed(() => {
|
||||||
|
const d = data.value?.instancesInfos;
|
||||||
|
if (!d || d.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out: Record<string, number> = {};
|
||||||
|
|
||||||
|
d.forEach((v) => {
|
||||||
|
const ver = v.meta?.version.replace(/[-\+].+$/g, '');
|
||||||
|
if (ver) {
|
||||||
|
if (!out[ver]) {
|
||||||
|
out[ver] = 1;
|
||||||
|
} else {
|
||||||
|
out[ver]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(out)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.reduce(
|
||||||
|
(r, [k, v]) => ({
|
||||||
|
...r,
|
||||||
|
[k]: v
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const avgVersionFocus = ref<number | undefined>();
|
||||||
|
|
||||||
|
function trunc(obj: Record<any, number>, ratio: number = 0.02) {
|
||||||
|
const sum = Object.values(obj).reduce((p, c) => p + c, 0);
|
||||||
|
const toBeTrunked = Object.values(obj).filter((v) => v / sum < ratio);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...Object.entries(obj)
|
||||||
|
.slice(0, (Object.values(obj).length - toBeTrunked.length))
|
||||||
|
.reduce(
|
||||||
|
(r, [k, v]) => ({
|
||||||
|
...r,
|
||||||
|
[k]: v
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
__others: toBeTrunked.reduce((p, c) => p + c, 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.kvRoot {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,6 +5,7 @@ clickToExpand: "(クリックして展開)"
|
||||||
copy: "コピー"
|
copy: "コピー"
|
||||||
share: "共有する"
|
share: "共有する"
|
||||||
note: "ノート"
|
note: "ノート"
|
||||||
|
other: "その他"
|
||||||
|
|
||||||
_error:
|
_error:
|
||||||
notFound: "ページが見つかりませんでした"
|
notFound: "ページが見つかりませんでした"
|
||||||
|
@ -102,6 +103,12 @@ _servers:
|
||||||
_system:
|
_system:
|
||||||
fetchError: "データの読み込みに失敗しました。後でもう一度お試しください。"
|
fetchError: "データの読み込みに失敗しました。後でもう一度お試しください。"
|
||||||
_statistics:
|
_statistics:
|
||||||
|
title: "サーバー統計"
|
||||||
|
description: "Misskeyサーバーの統計データをグラフでご紹介。"
|
||||||
|
viewFullStats: "詳しい統計を見る"
|
||||||
|
lang: "プライマリ言語"
|
||||||
|
registerAcceptance: "新規登録受付方式"
|
||||||
|
version: "バージョン"
|
||||||
notes: "ノート数"
|
notes: "ノート数"
|
||||||
users: "ユーザー数"
|
users: "ユーザー数"
|
||||||
servers: "サーバー数"
|
servers: "サーバー数"
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="relative px-6 py-8">
|
<div class="relative px-6 py-8">
|
||||||
<GDots class="absolute top-0 left-0 w-32 h-32 text-accent-600" />
|
<GDots class="absolute top-0 left-0 w-32 h-32 text-accent-600" />
|
||||||
<GDots class="absolute bottom-0 right-0 w-32 h-32 text-accent-600" />
|
<GDots class="absolute bottom-0 right-0 w-32 h-32 text-accent-600" />
|
||||||
<div class="relative bg-white dark:bg-slate-800 shadow-lg rounded-lg lg:w-64 p-6 space-y-4">
|
<div class="relative bg-white dark:bg-slate-800 shadow-lg rounded-lg w-full lg:w-80 p-6 space-y-4 break-words">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>{{ $t('_servers._statistics.notes') }}</dt>
|
<dt>{{ $t('_servers._statistics.notes') }}</dt>
|
||||||
<dd class="font-bold text-accent-600 text-2xl">{{ instancesStats?.notesCount?.toLocaleString() || $t('loading') }}</dd>
|
<dd class="font-bold text-accent-600 text-2xl">{{ instancesStats?.notesCount?.toLocaleString() || $t('loading') }}</dd>
|
||||||
|
@ -25,6 +25,9 @@
|
||||||
<dt>{{ $t('_servers._statistics.servers') }}</dt>
|
<dt>{{ $t('_servers._statistics.servers') }}</dt>
|
||||||
<dd class="font-bold text-accent-600 text-2xl">{{ instancesStats?.instancesCount?.toLocaleString() || $t('loading') }}</dd>
|
<dd class="font-bold text-accent-600 text-2xl">{{ instancesStats?.instancesCount?.toLocaleString() || $t('loading') }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
<div class="!mt-2">
|
||||||
|
<GNuxtLink class="hover:underline underline-offset-2" :to="localePath('/servers/stats/')">{{ $t('_servers._statistics.viewFullStats') }}<ArrowRightIco class="ml-1" /></GNuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -45,8 +48,10 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { InstancesStatsObj } from '@/types/instances-info';
|
import type { InstancesStatsObj } from '@/types/instances-info';
|
||||||
|
import ArrowRightIco from "bi/arrow-right.svg";
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
const localePath = useLocalePath();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const instancesStats = ref<InstancesStatsObj>();
|
const instancesStats = ref<InstancesStatsObj>();
|
41
pages/servers/stats.vue
Normal file
41
pages/servers/stats.vue
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<GHero>
|
||||||
|
<template #title>{{ $t('_servers._statistics.title') }}</template>
|
||||||
|
<template #description>
|
||||||
|
{{ $t('_servers._statistics.description') }}
|
||||||
|
</template>
|
||||||
|
<template #icon>
|
||||||
|
<div class="hidden lg:block relative px-6 py-8">
|
||||||
|
<GDots class="absolute top-0 left-0 w-32 h-32 text-accent-600" />
|
||||||
|
<GDots class="absolute bottom-0 right-0 w-32 h-32 text-accent-600" />
|
||||||
|
<div class="relative lg:w-64">
|
||||||
|
<img class="drop-shadow-xl" src="/img/emojis/chart_increasing_3d.png" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</GHero>
|
||||||
|
<div class="pb-12 lg:mt-12 pt-6 bg-white dark:bg-slate-950 min-h-screen">
|
||||||
|
<ClientOnly>
|
||||||
|
<ServersStatsViewer />
|
||||||
|
<template #fallback>
|
||||||
|
<div class="container mx-auto max-w-screen-xl p-6">
|
||||||
|
<MkLoading class="mx-auto text-accent-600"></MkLoading>
|
||||||
|
<p class="text-center">{{ $t('loading') }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
route.meta.title = t('_servers._statistics.title');
|
||||||
|
route.meta.description = t('_servers._statistics.description');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
BIN
public/img/emojis/chart_increasing_3d.png
Normal file
BIN
public/img/emojis/chart_increasing_3d.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
|
@ -38,6 +38,7 @@ export default <Config> {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
...defaultTheme.fontFamily,
|
||||||
'title': ['Capriola', 'var(--mi-localized-font, \'\')', ...defaultTheme.fontFamily.sans],
|
'title': ['Capriola', 'var(--mi-localized-font, \'\')', ...defaultTheme.fontFamily.sans],
|
||||||
'sans': ['Nunito', 'var(--mi-localized-font, \'\')', ...defaultTheme.fontFamily.sans],
|
'sans': ['Nunito', 'var(--mi-localized-font, \'\')', ...defaultTheme.fontFamily.sans],
|
||||||
'content-sans': ['Nunito', 'var(--mi-localized-font-p, var(--mi-localized-font))', ...defaultTheme.fontFamily.sans],
|
'content-sans': ['Nunito', 'var(--mi-localized-font-p, var(--mi-localized-font))', ...defaultTheme.fontFamily.sans],
|
||||||
|
|
Loading…
Reference in a new issue