diff --git a/packages/client/src/components/global/page-header.vue b/packages/client/src/components/global/page-header.vue index c01631c6a3..0ad7699834 100644 --- a/packages/client/src/components/global/page-header.vue +++ b/packages/client/src/components/global/page-header.vue @@ -12,16 +12,17 @@ {{ metadata.subtitle }} </div> <div v-if="narrow && hasTabs" class="subtitle activeTab"> - {{ tabs.find(tab => tab.active)?.title }} + {{ tabs.find(tab => tab.key === props.tab)?.title }} <i class="chevron fas fa-chevron-down"></i> </div> </div> </div> <div v-if="!narrow || hideTitle" class="tabs"> - <button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> + <button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)"> <i v-if="tab.icon" class="icon" :class="tab.icon"></i> <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> </button> + <div ref="tabHighlightEl" class="highlight"></div> </div> </template> <div class="buttons right"> @@ -33,22 +34,25 @@ </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted, ref, inject } from 'vue'; +import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue'; import tinycolor from 'tinycolor2'; import { popupMenu } from '@/os'; import { scrollToTop } from '@/scripts/scroll'; import { i18n } from '@/i18n'; import { globalEvents } from '@/events'; -import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; +import { injectPageMetadata } from '@/scripts/page-metadata'; + +type Tab = { + key?: string | null; + title: string; + icon?: string; + iconOnly?: boolean; + onClick?: (ev: MouseEvent) => void; +}; const props = defineProps<{ - tabs?: { - title: string; - active: boolean; - icon?: string; - iconOnly?: boolean; - onClick: () => void; - }[]; + tabs?: Tab[]; + tab?: string; actions?: { text: string; icon: string; @@ -57,12 +61,18 @@ const props = defineProps<{ thin?: boolean; }>(); +const emit = defineEmits<{ + (ev: 'update:tab', key: string); +}>(); + const metadata = injectPageMetadata(); const hideTitle = inject('shouldOmitHeaderTitle', false); const thin_ = props.thin || inject('shouldHeaderThin', false); const el = $ref<HTMLElement | null>(null); +const tabRefs = {}; +const tabHighlightEl = $ref<HTMLElement | null>(null); const bg = ref(null); let narrow = $ref(false); const height = ref(0); @@ -80,7 +90,10 @@ const showTabsPopup = (ev: MouseEvent) => { const menu = props.tabs.map(tab => ({ text: tab.title, icon: tab.icon, - action: tab.onClick, + active: tab.key != null && tab.key === props.tab, + action: (ev) => { + onTabClick(tab, ev); + }, })); popupMenu(menu, ev.currentTarget ?? ev.target); }; @@ -93,6 +106,20 @@ const onClick = () => { scrollToTop(el, { behavior: 'smooth' }); }; +function onTabMousedown(tab: Tab, ev: MouseEvent): void { + // ユーザビリティの観点からmousedown時にはonClickは呼ばない + if (tab.key) { + emit('update:tab', tab.key); + } +} + +function onTabClick(tab: Tab, ev: MouseEvent): void { + if (tab.onClick) tab.onClick(ev); + if (tab.key) { + emit('update:tab', tab.key); + } +} + const calcBg = () => { const rawBg = metadata?.bg || 'var(--bg)'; const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); @@ -106,6 +133,20 @@ onMounted(() => { calcBg(); globalEvents.on('themeChanged', calcBg); + watch(() => props.tab, () => { + const tabEl = tabRefs[props.tab]; + if (tabEl && tabHighlightEl) { + // offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある + // https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 + const parentRect = tabEl.parentElement.getBoundingClientRect(); + const rect = tabEl.getBoundingClientRect(); + tabHighlightEl.style.width = rect.width + 'px'; + tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px'; + } + }, { + immediate: true, + }); + if (el && el.parentElement) { narrow = el.parentElement.offsetWidth < 500; ro = new ResizeObserver((entries, observer) => { @@ -257,6 +298,7 @@ onUnmounted(() => { } > .tabs { + position: relative; margin-left: 16px; font-size: 0.8em; overflow: auto; @@ -276,25 +318,22 @@ onUnmounted(() => { &.active { opacity: 1; - - &:after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - right: 0; - margin: 0 auto; - width: 100%; - height: 3px; - background: var(--accent); - } } > .icon + .title { margin-left: 8px; } } + + > .highlight { + position: absolute; + bottom: 0; + height: 3px; + background: var(--accent); + border-radius: 999px; + transition: all 0.2s ease; + pointer-events: none; + } } } </style> diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index 20497c86fc..c0226bdb6c 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> <div class="_formRoot"> <div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> @@ -98,14 +98,12 @@ const initStats = () => os.api('stats', { const headerActions = $computed(() => []); const headerTabs = $computed(() => [{ - active: tab === 'overview', + key: 'overview', title: i18n.ts.overview, - onClick: () => { tab = 'overview'; }, }, { - active: tab === 'charts', + key: 'charts', title: i18n.ts.charts, icon: 'fas fa-chart-bar', - onClick: () => { tab = 'charts'; }, }]); definePageMetadata(computed(() => ({ diff --git a/packages/client/src/pages/admin-file.vue b/packages/client/src/pages/admin-file.vue index 7273ddce6a..7fb7cc1c87 100644 --- a/packages/client/src/pages/admin-file.vue +++ b/packages/client/src/pages/admin-file.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer v-if="file" :content-max="500" :margin-min="16" :margin-max="32"> <div v-if="tab === 'overview'" class="cxqhhsmd _formRoot"> <a class="_formBlock thumbnail" :href="file.url" target="_blank"> @@ -103,15 +103,13 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ - active: tab === 'overview', + key: 'overview', title: i18n.ts.overview, icon: 'fas fa-info-circle', - onClick: () => { tab = 'overview'; }, }, { - active: tab === 'raw', + key: 'raw', title: 'Raw data', icon: 'fas fa-code', - onClick: () => { tab = 'raw'; }, }]); definePageMetadata(computed(() => ({ diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue index 9e11d065d9..59c5ce4f47 100644 --- a/packages/client/src/pages/admin/_header_.vue +++ b/packages/client/src/pages/admin/_header_.vue @@ -9,10 +9,11 @@ </div> </div> <div class="tabs"> - <button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> + <button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)"> <i v-if="tab.icon" class="icon" :class="tab.icon"></i> <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> </button> + <div ref="tabHighlightEl" class="highlight"></div> </div> </template> <div class="buttons right"> @@ -27,7 +28,7 @@ </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted, ref, inject } from 'vue'; +import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue'; import tinycolor from 'tinycolor2'; import { popupMenu } from '@/os'; import { url } from '@/config'; @@ -35,16 +36,19 @@ import { scrollToTop } from '@/scripts/scroll'; import MkButton from '@/components/ui/button.vue'; import { i18n } from '@/i18n'; import { globalEvents } from '@/events'; -import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; +import { injectPageMetadata } from '@/scripts/page-metadata'; + +type Tab = { + key?: string | null; + title: string; + icon?: string; + iconOnly?: boolean; + onClick?: (ev: MouseEvent) => void; +}; const props = defineProps<{ - tabs?: { - title: string; - active: boolean; - icon?: string; - iconOnly?: boolean; - onClick: () => void; - }[]; + tabs?: Tab[]; + tab?: string; actions?: { text: string; icon: string; @@ -54,9 +58,15 @@ const props = defineProps<{ thin?: boolean; }>(); +const emit = defineEmits<{ + (ev: 'update:tab', key: string); +}>(); + const metadata = injectPageMetadata(); const el = ref<HTMLElement>(null); +const tabRefs = {}; +const tabHighlightEl = $ref<HTMLElement | null>(null); const bg = ref(null); const height = ref(0); const hasTabs = computed(() => { @@ -71,7 +81,10 @@ const showTabsPopup = (ev: MouseEvent) => { const menu = props.tabs.map(tab => ({ text: tab.title, icon: tab.icon, - action: tab.onClick, + active: tab.key != null && tab.key === props.tab, + action: (ev) => { + onTabClick(tab, ev); + }, })); popupMenu(menu, ev.currentTarget ?? ev.target); }; @@ -84,6 +97,20 @@ const onClick = () => { scrollToTop(el.value, { behavior: 'smooth' }); }; +function onTabMousedown(tab: Tab, ev: MouseEvent): void { + // ユーザビリティの観点からmousedown時にはonClickは呼ばない + if (tab.key) { + emit('update:tab', tab.key); + } +} + +function onTabClick(tab: Tab, ev: MouseEvent): void { + if (tab.onClick) tab.onClick(ev); + if (tab.key) { + emit('update:tab', tab.key); + } +} + const calcBg = () => { const rawBg = metadata?.bg || 'var(--bg)'; const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); @@ -94,6 +121,20 @@ const calcBg = () => { onMounted(() => { calcBg(); globalEvents.on('themeChanged', calcBg); + + watch(() => props.tab, () => { + const tabEl = tabRefs[props.tab]; + if (tabEl && tabHighlightEl) { + // offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある + // https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 + const parentRect = tabEl.parentElement.getBoundingClientRect(); + const rect = tabEl.getBoundingClientRect(); + tabHighlightEl.style.width = rect.width + 'px'; + tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px'; + } + }, { + immediate: true, + }); }); onUnmounted(() => { @@ -206,6 +247,7 @@ onUnmounted(() => { } > .tabs { + position: relative; margin-left: 16px; font-size: 0.8em; overflow: auto; @@ -225,25 +267,22 @@ onUnmounted(() => { &.active { opacity: 1; - - &:after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - right: 0; - margin: 0 auto; - width: 100%; - height: 3px; - background: var(--accent); - } } > .icon + .title { margin-left: 8px; } } + + > .highlight { + position: absolute; + bottom: 0; + height: 3px; + background: var(--accent); + border-radius: 999px; + transition: all 0.2s ease; + pointer-events: none; + } } } </style> diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index 9d6b56dbc5..868472ac9c 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -1,7 +1,7 @@ <template> <div> <MkStickyContainer> - <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs" v-model:tab="tab"/></template> <MkSpacer :content-max="900"> <div class="ogwlenmc"> <div v-if="tab === 'local'" class="local"> @@ -282,13 +282,11 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ - active: tab.value === 'local', + key: 'local', title: i18n.ts.local, - onClick: () => { tab.value = 'local'; }, }, { - active: tab.value === 'remote', + key: 'remote', title: i18n.ts.remote, - onClick: () => { tab.value = 'remote'; }, }]); definePageMetadata(computed(() => ({ diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index 89d23350f2..c48a64a1e7 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="700"> <div v-if="tab === 'featured'" class="_content grwlizim featured"> <MkPagination v-slot="{items}" :pagination="featuredPagination"> @@ -59,20 +59,17 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ - active: tab === 'featured', + key: 'featured', title: i18n.ts._channel.featured, icon: 'fas fa-fire-alt', - onClick: () => { tab = 'featured'; }, }, { - active: tab === 'following', + key: 'following', title: i18n.ts._channel.following, icon: 'fas fa-heart', - onClick: () => { tab = 'following'; }, }, { - active: tab === 'owned', + key: 'owned', title: i18n.ts._channel.owned, icon: 'fas fa-edit', - onClick: () => { tab = 'owned'; }, }]); definePageMetadata(computed(() => ({ diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue index 26e201cd99..cd0dba7817 100644 --- a/packages/client/src/pages/explore.vue +++ b/packages/client/src/pages/explore.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="1200"> <div class="lznhrdub"> <div v-if="tab === 'local'"> @@ -178,17 +178,14 @@ os.api('stats').then(_stats => { const headerActions = $computed(() => []); const headerTabs = $computed(() => [{ - active: tab === 'local', + key: 'local', title: i18n.ts.local, - onClick: () => { tab = 'local'; }, }, { - active: tab === 'remote', + key: 'remote', title: i18n.ts.remote, - onClick: () => { tab = 'remote'; }, }, { - active: tab === 'search', + key: 'search', title: i18n.ts.search, - onClick: () => { tab = 'search'; }, }]); definePageMetadata(computed(() => ({ diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index cca04aca93..148eab6fcd 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32"> <div v-if="tab === 'overview'" class="_formRoot"> <div class="fnfelxur"> @@ -183,20 +183,17 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ - active: tab === 'overview', + key: 'overview', title: i18n.ts.overview, icon: 'fas fa-info-circle', - onClick: () => { tab = 'overview'; }, }, { - active: tab === 'chart', + key: 'chart', title: i18n.ts.charts, icon: 'fas fa-chart-simple', - onClick: () => { tab = 'chart'; }, }, { - active: tab === 'raw', + key: 'raw', title: 'Raw data', icon: 'fas fa-code', - onClick: () => { tab = 'raw'; }, }]); definePageMetadata({ diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue index 3d1014b3cd..52cb298fa3 100644 --- a/packages/client/src/pages/notifications.vue +++ b/packages/client/src/pages/notifications.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="800"> <div class="clupoqwt"> <XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> @@ -52,13 +52,11 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ - active: tab === 'all', + key: 'all', title: i18n.ts.all, - onClick: () => { tab = 'all'; }, }, { - active: tab === 'unread', + key: 'unread', title: i18n.ts.unread, - onClick: () => { tab = 'unread'; }, }]); definePageMetadata(computed(() => ({ diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue index c09d9af734..c38286c1d3 100644 --- a/packages/client/src/pages/page-editor/page-editor.vue +++ b/packages/client/src/pages/page-editor/page-editor.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs" v-model:tab="tab"/></template> <MkSpacer :content-max="700"> <div class="jqqmcavi"> <MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton> @@ -411,25 +411,21 @@ init(); const headerActions = $computed(() => []); const headerTabs = $computed(() => [{ - active: tab === 'settings', + key: 'settings', title: i18n.ts._pages.pageSetting, icon: 'fas fa-cog', - onClick: () => { tab = 'settings'; }, }, { - active: tab === 'contents', + key: 'contents', title: i18n.ts._pages.contents, icon: 'fas fa-sticky-note', - onClick: () => { tab = 'contents'; }, }, { - active: tab === 'variables', + key: 'variables', title: i18n.ts._pages.variables, icon: 'fas fa-magic', - onClick: () => { tab = 'variables'; }, }, { - active: tab === 'script', + key: 'script', title: i18n.ts.script, icon: 'fas fa-code', - onClick: () => { tab = 'script'; }, }]); definePageMetadata(computed(() => { diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue index 541c968ff4..16aeae2f56 100644 --- a/packages/client/src/pages/pages.vue +++ b/packages/client/src/pages/pages.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="700"> <div v-if="tab === 'featured'" class="rknalgpo"> <MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> @@ -61,20 +61,17 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ - active: tab === 'featured', + key: 'featured', title: i18n.ts._pages.featured, icon: 'fas fa-fire-alt', - onClick: () => { tab = 'featured'; }, }, { - active: tab === 'my', + key: 'my', title: i18n.ts._pages.my, icon: 'fas fa-edit', - onClick: () => { tab = 'my'; }, }, { - active: tab === 'liked', + key: 'liked', title: i18n.ts._pages.liked, icon: 'fas fa-heart', - onClick: () => { tab = 'liked'; }, }]); definePageMetadata(computed(() => ({ diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 111451632c..004c29c56b 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="800"> <div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf"> <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> @@ -45,7 +45,7 @@ const tlComponent = $ref<InstanceType<typeof XTimeline>>(); const rootEl = $ref<HTMLElement>(); let queue = $ref(0); -const src = $computed(() => defaultStore.reactiveState.tl.value.src); +const src = $computed({ get: () => defaultStore.reactiveState.tl.value.src, set: (x) => saveSrc(x) }); watch ($$(src), () => queue = 0); @@ -112,29 +112,25 @@ function focus(): void { const headerActions = $computed(() => []); const headerTabs = $computed(() => [{ - active: src === 'home', + key: 'home', title: i18n.ts._timelines.home, icon: 'fas fa-home', iconOnly: true, - onClick: () => { saveSrc('home'); }, }, ...(isLocalTimelineAvailable ? [{ - active: src === 'local', + key: 'local', title: i18n.ts._timelines.local, icon: 'fas fa-comments', iconOnly: true, - onClick: () => { saveSrc('local'); }, }, { - active: src === 'social', + key: 'social', title: i18n.ts._timelines.social, icon: 'fas fa-share-alt', iconOnly: true, - onClick: () => { saveSrc('social'); }, }] : []), ...(isGlobalTimelineAvailable ? [{ - active: src === 'global', + key: 'global', title: i18n.ts._timelines.global, icon: 'fas fa-globe', iconOnly: true, - onClick: () => { saveSrc('global'); }, }] : []), { icon: 'fas fa-list-ul', title: i18n.ts.lists, diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 233a8857fc..67fc5ba7e3 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -1,6 +1,6 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> <div v-if="tab === 'overview'" class="_formRoot"> @@ -234,20 +234,17 @@ watch(() => user, () => { const headerActions = $computed(() => []); const headerTabs = $computed(() => [{ - active: tab === 'overview', + key: 'overview', title: i18n.ts.overview, icon: 'fas fa-info-circle', - onClick: () => { tab = 'overview'; }, }, { - active: tab === 'chart', + key: 'chart', title: i18n.ts.charts, icon: 'fas fa-chart-simple', - onClick: () => { tab = 'chart'; }, }, { - active: tab === 'raw', + key: 'raw', title: 'Raw data', icon: 'fas fa-code', - onClick: () => { tab = 'raw'; }, }]); definePageMetadata(computed(() => ({