refactor(client): refactor header tab handling
This commit is contained in:
		
							parent
							
								
									e44cb42de4
								
							
						
					
					
						commit
						85365da69e
					
				
					 13 changed files with 170 additions and 123 deletions
				
			
		|  | @ -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> | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
|  | @ -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({ | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
|  | @ -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(() => { | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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(() => ({ | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue