101 lines
2.7 KiB
Vue
101 lines
2.7 KiB
Vue
<script setup lang="ts">
|
||
import { t, formatNumber } from '@/i18n'
|
||
|
||
import { computed, ref } from 'vue'
|
||
import { useScrollEdges } from '@/composables/useScrollEdges'
|
||
import type { ListTab, ListTabKey } from '@/lib/storyList'
|
||
import { UNDERLINE_TAB_TRIGGER } from '@/lib/tabStyles'
|
||
|
||
const SCROLL_STEP_PX = 96
|
||
const EMPTY_COUNT = '–'
|
||
|
||
const props = defineProps<{
|
||
tabs: ListTab[]
|
||
active: ListTabKey
|
||
}>()
|
||
|
||
const emit = defineEmits<{ select: [key: ListTabKey] }>()
|
||
|
||
const scroller = ref<HTMLElement | null>(null)
|
||
const { hiddenBefore, hiddenAfter } = useScrollEdges(scroller)
|
||
|
||
const model = computed({
|
||
get: () => props.active,
|
||
set: (value: ListTabKey) => emit('select', value),
|
||
})
|
||
|
||
const items = computed(() =>
|
||
props.tabs.map((tab) => ({
|
||
label: tab.label,
|
||
value: tab.key,
|
||
count: tab.count === null ? EMPTY_COUNT : formatNumber(tab.count),
|
||
})),
|
||
)
|
||
|
||
function scroll(direction: -1 | 1) {
|
||
scroller.value?.scrollBy({ left: direction * SCROLL_STEP_PX })
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="relative flex items-center gap-1 border-b border-default">
|
||
<UButton
|
||
color="neutral"
|
||
variant="ghost"
|
||
icon="i-lucide-chevron-left"
|
||
:aria-label="t('Scroll tabs left')"
|
||
:disabled="!hiddenBefore"
|
||
class="size-11 shrink-0 justify-center md:hidden"
|
||
@click="scroll(-1)"
|
||
/>
|
||
<div class="relative min-w-0 flex-1">
|
||
<div
|
||
ref="scroller"
|
||
class="overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||
>
|
||
<UTabs
|
||
v-model="model"
|
||
:items="items"
|
||
variant="link"
|
||
color="neutral"
|
||
:content="false"
|
||
:aria-label="t('Article views')"
|
||
class="w-max"
|
||
:ui="{
|
||
list: 'gap-4 border-b-0 p-0 md:gap-6',
|
||
indicator: 'hidden',
|
||
trigger: `${UNDERLINE_TAB_TRIGGER} after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 data-[state=active]:after:bg-inverted shrink-0 whitespace-nowrap md:h-9.5 max-md:h-11 max-md:text-base`,
|
||
}"
|
||
>
|
||
<template #default="{ item }">
|
||
<span class="truncate">{{ item.label }}</span
|
||
>{{ ' '
|
||
}}<span class="font-normal text-muted">{{
|
||
(item as (typeof items)[number]).count
|
||
}}</span>
|
||
</template>
|
||
</UTabs>
|
||
</div>
|
||
<div
|
||
v-if="hiddenAfter"
|
||
aria-hidden="true"
|
||
class="pointer-events-none absolute inset-y-0 right-0 w-7 bg-linear-to-r from-transparent to-(--ui-bg) md:hidden"
|
||
/>
|
||
<div
|
||
v-if="hiddenBefore"
|
||
aria-hidden="true"
|
||
class="pointer-events-none absolute inset-y-0 left-0 w-7 bg-linear-to-l from-transparent to-(--ui-bg) md:hidden"
|
||
/>
|
||
</div>
|
||
<UButton
|
||
color="neutral"
|
||
variant="ghost"
|
||
icon="i-lucide-chevron-right"
|
||
:aria-label="t('Scroll tabs right')"
|
||
:disabled="!hiddenAfter"
|
||
class="size-11 shrink-0 justify-center md:hidden"
|
||
@click="scroll(1)"
|
||
/>
|
||
</div>
|
||
</template>
|