refactor(client): refactor header tab handling
This commit is contained in:
		| @@ -12,16 +12,17 @@ | |||||||
| 					{{ metadata.subtitle }} | 					{{ metadata.subtitle }} | ||||||
| 				</div> | 				</div> | ||||||
| 				<div v-if="narrow && hasTabs" class="subtitle activeTab"> | 				<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> | 					<i class="chevron fas fa-chevron-down"></i> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div v-if="!narrow || hideTitle" class="tabs"> | 		<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> | 				<i v-if="tab.icon" class="icon" :class="tab.icon"></i> | ||||||
| 				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> | 				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> | ||||||
| 			</button> | 			</button> | ||||||
|  | 			<div ref="tabHighlightEl" class="highlight"></div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</template> | 	</template> | ||||||
| 	<div class="buttons right"> | 	<div class="buttons right"> | ||||||
| @@ -33,22 +34,25 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <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 tinycolor from 'tinycolor2'; | ||||||
| import { popupMenu } from '@/os'; | import { popupMenu } from '@/os'; | ||||||
| import { scrollToTop } from '@/scripts/scroll'; | import { scrollToTop } from '@/scripts/scroll'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { globalEvents } from '@/events'; | import { globalEvents } from '@/events'; | ||||||
| import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; | import { injectPageMetadata } from '@/scripts/page-metadata'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | type Tab = { | ||||||
| 	tabs?: { | 	key?: string | null; | ||||||
| 	title: string; | 	title: string; | ||||||
| 		active: boolean; |  | ||||||
| 	icon?: string; | 	icon?: string; | ||||||
| 	iconOnly?: boolean; | 	iconOnly?: boolean; | ||||||
| 		onClick: () => void; | 	onClick?: (ev: MouseEvent) => void; | ||||||
| 	}[]; | }; | ||||||
|  |  | ||||||
|  | const props = defineProps<{ | ||||||
|  | 	tabs?: Tab[]; | ||||||
|  | 	tab?: string; | ||||||
| 	actions?: { | 	actions?: { | ||||||
| 		text: string; | 		text: string; | ||||||
| 		icon: string; | 		icon: string; | ||||||
| @@ -57,12 +61,18 @@ const props = defineProps<{ | |||||||
| 	thin?: boolean; | 	thin?: boolean; | ||||||
| }>(); | }>(); | ||||||
|  |  | ||||||
|  | const emit = defineEmits<{ | ||||||
|  | 	(ev: 'update:tab', key: string); | ||||||
|  | }>(); | ||||||
|  |  | ||||||
| const metadata = injectPageMetadata(); | const metadata = injectPageMetadata(); | ||||||
|  |  | ||||||
| const hideTitle = inject('shouldOmitHeaderTitle', false); | const hideTitle = inject('shouldOmitHeaderTitle', false); | ||||||
| const thin_ = props.thin || inject('shouldHeaderThin', false); | const thin_ = props.thin || inject('shouldHeaderThin', false); | ||||||
|  |  | ||||||
| const el = $ref<HTMLElement | null>(null); | const el = $ref<HTMLElement | null>(null); | ||||||
|  | const tabRefs = {}; | ||||||
|  | const tabHighlightEl = $ref<HTMLElement | null>(null); | ||||||
| const bg = ref(null); | const bg = ref(null); | ||||||
| let narrow = $ref(false); | let narrow = $ref(false); | ||||||
| const height = ref(0); | const height = ref(0); | ||||||
| @@ -80,7 +90,10 @@ const showTabsPopup = (ev: MouseEvent) => { | |||||||
| 	const menu = props.tabs.map(tab => ({ | 	const menu = props.tabs.map(tab => ({ | ||||||
| 		text: tab.title, | 		text: tab.title, | ||||||
| 		icon: tab.icon, | 		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); | 	popupMenu(menu, ev.currentTarget ?? ev.target); | ||||||
| }; | }; | ||||||
| @@ -93,6 +106,20 @@ const onClick = () => { | |||||||
| 	scrollToTop(el, { behavior: 'smooth' }); | 	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 calcBg = () => { | ||||||
| 	const rawBg = metadata?.bg || 'var(--bg)'; | 	const rawBg = metadata?.bg || 'var(--bg)'; | ||||||
| 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); | 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); | ||||||
| @@ -106,6 +133,20 @@ onMounted(() => { | |||||||
| 	calcBg(); | 	calcBg(); | ||||||
| 	globalEvents.on('themeChanged', 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) { | 	if (el && el.parentElement) { | ||||||
| 		narrow = el.parentElement.offsetWidth < 500; | 		narrow = el.parentElement.offsetWidth < 500; | ||||||
| 		ro = new ResizeObserver((entries, observer) => { | 		ro = new ResizeObserver((entries, observer) => { | ||||||
| @@ -257,6 +298,7 @@ onUnmounted(() => { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	> .tabs { | 	> .tabs { | ||||||
|  | 		position: relative; | ||||||
| 		margin-left: 16px; | 		margin-left: 16px; | ||||||
| 		font-size: 0.8em; | 		font-size: 0.8em; | ||||||
| 		overflow: auto; | 		overflow: auto; | ||||||
| @@ -276,25 +318,22 @@ onUnmounted(() => { | |||||||
|  |  | ||||||
| 			&.active { | 			&.active { | ||||||
| 				opacity: 1; | 				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 { | 			> .icon + .title { | ||||||
| 				margin-left: 8px; | 				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> | </style> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> | ||||||
| 		<div class="_formRoot"> | 		<div class="_formRoot"> | ||||||
| 			<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> | 			<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> | ||||||
| @@ -98,14 +98,12 @@ const initStats = () => os.api('stats', { | |||||||
| const headerActions = $computed(() => []); | const headerActions = $computed(() => []); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'overview', | 	key: 'overview', | ||||||
| 	title: i18n.ts.overview, | 	title: i18n.ts.overview, | ||||||
| 	onClick: () => { tab = 'overview'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'charts', | 	key: 'charts', | ||||||
| 	title: i18n.ts.charts, | 	title: i18n.ts.charts, | ||||||
| 	icon: 'fas fa-chart-bar', | 	icon: 'fas fa-chart-bar', | ||||||
| 	onClick: () => { tab = 'charts'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer v-if="file" :content-max="500" :margin-min="16" :margin-max="32"> | ||||||
| 		<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot"> | 		<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot"> | ||||||
| 			<a class="_formBlock thumbnail" :href="file.url" target="_blank"> | 			<a class="_formBlock thumbnail" :href="file.url" target="_blank"> | ||||||
| @@ -103,15 +103,13 @@ const headerActions = $computed(() => [{ | |||||||
| }]); | }]); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'overview', | 	key: 'overview', | ||||||
| 	title: i18n.ts.overview, | 	title: i18n.ts.overview, | ||||||
| 	icon: 'fas fa-info-circle', | 	icon: 'fas fa-info-circle', | ||||||
| 	onClick: () => { tab = 'overview'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'raw', | 	key: 'raw', | ||||||
| 	title: 'Raw data', | 	title: 'Raw data', | ||||||
| 	icon: 'fas fa-code', | 	icon: 'fas fa-code', | ||||||
| 	onClick: () => { tab = 'raw'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
| @@ -9,10 +9,11 @@ | |||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="tabs"> | 		<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> | 				<i v-if="tab.icon" class="icon" :class="tab.icon"></i> | ||||||
| 				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> | 				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> | ||||||
| 			</button> | 			</button> | ||||||
|  | 			<div ref="tabHighlightEl" class="highlight"></div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</template> | 	</template> | ||||||
| 	<div class="buttons right"> | 	<div class="buttons right"> | ||||||
| @@ -27,7 +28,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <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 tinycolor from 'tinycolor2'; | ||||||
| import { popupMenu } from '@/os'; | import { popupMenu } from '@/os'; | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
| @@ -35,16 +36,19 @@ import { scrollToTop } from '@/scripts/scroll'; | |||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { globalEvents } from '@/events'; | import { globalEvents } from '@/events'; | ||||||
| import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; | import { injectPageMetadata } from '@/scripts/page-metadata'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | type Tab = { | ||||||
| 	tabs?: { | 	key?: string | null; | ||||||
| 	title: string; | 	title: string; | ||||||
| 		active: boolean; |  | ||||||
| 	icon?: string; | 	icon?: string; | ||||||
| 	iconOnly?: boolean; | 	iconOnly?: boolean; | ||||||
| 		onClick: () => void; | 	onClick?: (ev: MouseEvent) => void; | ||||||
| 	}[]; | }; | ||||||
|  |  | ||||||
|  | const props = defineProps<{ | ||||||
|  | 	tabs?: Tab[]; | ||||||
|  | 	tab?: string; | ||||||
| 	actions?: { | 	actions?: { | ||||||
| 		text: string; | 		text: string; | ||||||
| 		icon: string; | 		icon: string; | ||||||
| @@ -54,9 +58,15 @@ const props = defineProps<{ | |||||||
| 	thin?: boolean; | 	thin?: boolean; | ||||||
| }>(); | }>(); | ||||||
|  |  | ||||||
|  | const emit = defineEmits<{ | ||||||
|  | 	(ev: 'update:tab', key: string); | ||||||
|  | }>(); | ||||||
|  |  | ||||||
| const metadata = injectPageMetadata(); | const metadata = injectPageMetadata(); | ||||||
|  |  | ||||||
| const el = ref<HTMLElement>(null); | const el = ref<HTMLElement>(null); | ||||||
|  | const tabRefs = {}; | ||||||
|  | const tabHighlightEl = $ref<HTMLElement | null>(null); | ||||||
| const bg = ref(null); | const bg = ref(null); | ||||||
| const height = ref(0); | const height = ref(0); | ||||||
| const hasTabs = computed(() => { | const hasTabs = computed(() => { | ||||||
| @@ -71,7 +81,10 @@ const showTabsPopup = (ev: MouseEvent) => { | |||||||
| 	const menu = props.tabs.map(tab => ({ | 	const menu = props.tabs.map(tab => ({ | ||||||
| 		text: tab.title, | 		text: tab.title, | ||||||
| 		icon: tab.icon, | 		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); | 	popupMenu(menu, ev.currentTarget ?? ev.target); | ||||||
| }; | }; | ||||||
| @@ -84,6 +97,20 @@ const onClick = () => { | |||||||
| 	scrollToTop(el.value, { behavior: 'smooth' }); | 	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 calcBg = () => { | ||||||
| 	const rawBg = metadata?.bg || 'var(--bg)'; | 	const rawBg = metadata?.bg || 'var(--bg)'; | ||||||
| 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); | 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); | ||||||
| @@ -94,6 +121,20 @@ const calcBg = () => { | |||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	calcBg(); | 	calcBg(); | ||||||
| 	globalEvents.on('themeChanged', 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(() => { | onUnmounted(() => { | ||||||
| @@ -206,6 +247,7 @@ onUnmounted(() => { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	> .tabs { | 	> .tabs { | ||||||
|  | 		position: relative; | ||||||
| 		margin-left: 16px; | 		margin-left: 16px; | ||||||
| 		font-size: 0.8em; | 		font-size: 0.8em; | ||||||
| 		overflow: auto; | 		overflow: auto; | ||||||
| @@ -225,25 +267,22 @@ onUnmounted(() => { | |||||||
|  |  | ||||||
| 			&.active { | 			&.active { | ||||||
| 				opacity: 1; | 				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 { | 			> .icon + .title { | ||||||
| 				margin-left: 8px; | 				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> | </style> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<MkStickyContainer> | 	<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"> | 		<MkSpacer :content-max="900"> | ||||||
| 			<div class="ogwlenmc"> | 			<div class="ogwlenmc"> | ||||||
| 				<div v-if="tab === 'local'" class="local"> | 				<div v-if="tab === 'local'" class="local"> | ||||||
| @@ -282,13 +282,11 @@ const headerActions = $computed(() => [{ | |||||||
| }]); | }]); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab.value === 'local', | 	key: 'local', | ||||||
| 	title: i18n.ts.local, | 	title: i18n.ts.local, | ||||||
| 	onClick: () => { tab.value = 'local'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab.value === 'remote', | 	key: 'remote', | ||||||
| 	title: i18n.ts.remote, | 	title: i18n.ts.remote, | ||||||
| 	onClick: () => { tab.value = 'remote'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer :content-max="700"> | ||||||
| 		<div v-if="tab === 'featured'" class="_content grwlizim featured"> | 		<div v-if="tab === 'featured'" class="_content grwlizim featured"> | ||||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||||
| @@ -59,20 +59,17 @@ const headerActions = $computed(() => [{ | |||||||
| }]); | }]); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'featured', | 	key: 'featured', | ||||||
| 	title: i18n.ts._channel.featured, | 	title: i18n.ts._channel.featured, | ||||||
| 	icon: 'fas fa-fire-alt', | 	icon: 'fas fa-fire-alt', | ||||||
| 	onClick: () => { tab = 'featured'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'following', | 	key: 'following', | ||||||
| 	title: i18n.ts._channel.following, | 	title: i18n.ts._channel.following, | ||||||
| 	icon: 'fas fa-heart', | 	icon: 'fas fa-heart', | ||||||
| 	onClick: () => { tab = 'following'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'owned', | 	key: 'owned', | ||||||
| 	title: i18n.ts._channel.owned, | 	title: i18n.ts._channel.owned, | ||||||
| 	icon: 'fas fa-edit', | 	icon: 'fas fa-edit', | ||||||
| 	onClick: () => { tab = 'owned'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer :content-max="1200"> | ||||||
| 		<div class="lznhrdub"> | 		<div class="lznhrdub"> | ||||||
| 			<div v-if="tab === 'local'"> | 			<div v-if="tab === 'local'"> | ||||||
| @@ -178,17 +178,14 @@ os.api('stats').then(_stats => { | |||||||
| const headerActions = $computed(() => []); | const headerActions = $computed(() => []); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'local', | 	key: 'local', | ||||||
| 	title: i18n.ts.local, | 	title: i18n.ts.local, | ||||||
| 	onClick: () => { tab = 'local'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'remote', | 	key: 'remote', | ||||||
| 	title: i18n.ts.remote, | 	title: i18n.ts.remote, | ||||||
| 	onClick: () => { tab = 'remote'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'search', | 	key: 'search', | ||||||
| 	title: i18n.ts.search, | 	title: i18n.ts.search, | ||||||
| 	onClick: () => { tab = 'search'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32"> | ||||||
| 		<div v-if="tab === 'overview'" class="_formRoot"> | 		<div v-if="tab === 'overview'" class="_formRoot"> | ||||||
| 			<div class="fnfelxur"> | 			<div class="fnfelxur"> | ||||||
| @@ -183,20 +183,17 @@ const headerActions = $computed(() => [{ | |||||||
| }]); | }]); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'overview', | 	key: 'overview', | ||||||
| 	title: i18n.ts.overview, | 	title: i18n.ts.overview, | ||||||
| 	icon: 'fas fa-info-circle', | 	icon: 'fas fa-info-circle', | ||||||
| 	onClick: () => { tab = 'overview'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'chart', | 	key: 'chart', | ||||||
| 	title: i18n.ts.charts, | 	title: i18n.ts.charts, | ||||||
| 	icon: 'fas fa-chart-simple', | 	icon: 'fas fa-chart-simple', | ||||||
| 	onClick: () => { tab = 'chart'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'raw', | 	key: 'raw', | ||||||
| 	title: 'Raw data', | 	title: 'Raw data', | ||||||
| 	icon: 'fas fa-code', | 	icon: 'fas fa-code', | ||||||
| 	onClick: () => { tab = 'raw'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer :content-max="800"> | ||||||
| 		<div class="clupoqwt"> | 		<div class="clupoqwt"> | ||||||
| 			<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> | 			<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> | ||||||
| @@ -52,13 +52,11 @@ const headerActions = $computed(() => [{ | |||||||
| }]); | }]); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'all', | 	key: 'all', | ||||||
| 	title: i18n.ts.all, | 	title: i18n.ts.all, | ||||||
| 	onClick: () => { tab = 'all'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'unread', | 	key: 'unread', | ||||||
| 	title: i18n.ts.unread, | 	title: i18n.ts.unread, | ||||||
| 	onClick: () => { tab = 'unread'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer :content-max="700"> | ||||||
| 		<div class="jqqmcavi"> | 		<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> | 			<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 headerActions = $computed(() => []); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'settings', | 	key: 'settings', | ||||||
| 	title: i18n.ts._pages.pageSetting, | 	title: i18n.ts._pages.pageSetting, | ||||||
| 	icon: 'fas fa-cog', | 	icon: 'fas fa-cog', | ||||||
| 	onClick: () => { tab = 'settings'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'contents', | 	key: 'contents', | ||||||
| 	title: i18n.ts._pages.contents, | 	title: i18n.ts._pages.contents, | ||||||
| 	icon: 'fas fa-sticky-note', | 	icon: 'fas fa-sticky-note', | ||||||
| 	onClick: () => { tab = 'contents'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'variables', | 	key: 'variables', | ||||||
| 	title: i18n.ts._pages.variables, | 	title: i18n.ts._pages.variables, | ||||||
| 	icon: 'fas fa-magic', | 	icon: 'fas fa-magic', | ||||||
| 	onClick: () => { tab = 'variables'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'script', | 	key: 'script', | ||||||
| 	title: i18n.ts.script, | 	title: i18n.ts.script, | ||||||
| 	icon: 'fas fa-code', | 	icon: 'fas fa-code', | ||||||
| 	onClick: () => { tab = 'script'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => { | definePageMetadata(computed(() => { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer :content-max="700"> | ||||||
| 		<div v-if="tab === 'featured'" class="rknalgpo"> | 		<div v-if="tab === 'featured'" class="rknalgpo"> | ||||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | 			<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | ||||||
| @@ -61,20 +61,17 @@ const headerActions = $computed(() => [{ | |||||||
| }]); | }]); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'featured', | 	key: 'featured', | ||||||
| 	title: i18n.ts._pages.featured, | 	title: i18n.ts._pages.featured, | ||||||
| 	icon: 'fas fa-fire-alt', | 	icon: 'fas fa-fire-alt', | ||||||
| 	onClick: () => { tab = 'featured'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'my', | 	key: 'my', | ||||||
| 	title: i18n.ts._pages.my, | 	title: i18n.ts._pages.my, | ||||||
| 	icon: 'fas fa-edit', | 	icon: 'fas fa-edit', | ||||||
| 	onClick: () => { tab = 'my'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'liked', | 	key: 'liked', | ||||||
| 	title: i18n.ts._pages.liked, | 	title: i18n.ts._pages.liked, | ||||||
| 	icon: 'fas fa-heart', | 	icon: 'fas fa-heart', | ||||||
| 	onClick: () => { tab = 'liked'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer :content-max="800"> | ||||||
| 		<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf"> | 		<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf"> | ||||||
| 			<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> | 			<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>(); | const rootEl = $ref<HTMLElement>(); | ||||||
|  |  | ||||||
| let queue = $ref(0); | 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); | watch ($$(src), () => queue = 0); | ||||||
|  |  | ||||||
| @@ -112,29 +112,25 @@ function focus(): void { | |||||||
| const headerActions = $computed(() => []); | const headerActions = $computed(() => []); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: src === 'home', | 	key: 'home', | ||||||
| 	title: i18n.ts._timelines.home, | 	title: i18n.ts._timelines.home, | ||||||
| 	icon: 'fas fa-home', | 	icon: 'fas fa-home', | ||||||
| 	iconOnly: true, | 	iconOnly: true, | ||||||
| 	onClick: () => { saveSrc('home'); }, |  | ||||||
| }, ...(isLocalTimelineAvailable ? [{ | }, ...(isLocalTimelineAvailable ? [{ | ||||||
| 	active: src === 'local', | 	key: 'local', | ||||||
| 	title: i18n.ts._timelines.local, | 	title: i18n.ts._timelines.local, | ||||||
| 	icon: 'fas fa-comments', | 	icon: 'fas fa-comments', | ||||||
| 	iconOnly: true, | 	iconOnly: true, | ||||||
| 	onClick: () => { saveSrc('local'); }, |  | ||||||
| }, { | }, { | ||||||
| 	active: src === 'social', | 	key: 'social', | ||||||
| 	title: i18n.ts._timelines.social, | 	title: i18n.ts._timelines.social, | ||||||
| 	icon: 'fas fa-share-alt', | 	icon: 'fas fa-share-alt', | ||||||
| 	iconOnly: true, | 	iconOnly: true, | ||||||
| 	onClick: () => { saveSrc('social'); }, |  | ||||||
| }] : []), ...(isGlobalTimelineAvailable ? [{ | }] : []), ...(isGlobalTimelineAvailable ? [{ | ||||||
| 	active: src === 'global', | 	key: 'global', | ||||||
| 	title: i18n.ts._timelines.global, | 	title: i18n.ts._timelines.global, | ||||||
| 	icon: 'fas fa-globe', | 	icon: 'fas fa-globe', | ||||||
| 	iconOnly: true, | 	iconOnly: true, | ||||||
| 	onClick: () => { saveSrc('global'); }, |  | ||||||
| }] : []), { | }] : []), { | ||||||
| 	icon: 'fas fa-list-ul', | 	icon: 'fas fa-list-ul', | ||||||
| 	title: i18n.ts.lists, | 	title: i18n.ts.lists, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <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"> | 	<MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> | ||||||
| 		<FormSuspense :p="init"> | 		<FormSuspense :p="init"> | ||||||
| 			<div v-if="tab === 'overview'" class="_formRoot"> | 			<div v-if="tab === 'overview'" class="_formRoot"> | ||||||
| @@ -234,20 +234,17 @@ watch(() => user, () => { | |||||||
| const headerActions = $computed(() => []); | const headerActions = $computed(() => []); | ||||||
|  |  | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'overview', | 	key: 'overview', | ||||||
| 	title: i18n.ts.overview, | 	title: i18n.ts.overview, | ||||||
| 	icon: 'fas fa-info-circle', | 	icon: 'fas fa-info-circle', | ||||||
| 	onClick: () => { tab = 'overview'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'chart', | 	key: 'chart', | ||||||
| 	title: i18n.ts.charts, | 	title: i18n.ts.charts, | ||||||
| 	icon: 'fas fa-chart-simple', | 	icon: 'fas fa-chart-simple', | ||||||
| 	onClick: () => { tab = 'chart'; }, |  | ||||||
| }, { | }, { | ||||||
| 	active: tab === 'raw', | 	key: 'raw', | ||||||
| 	title: 'Raw data', | 	title: 'Raw data', | ||||||
| 	icon: 'fas fa-code', | 	icon: 'fas fa-code', | ||||||
| 	onClick: () => { tab = 'raw'; }, |  | ||||||
| }]); | }]); | ||||||
|  |  | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo