Files
AstralRinth/apps/frontend/src/components/ui/NavTabs.vue
Prospector c39bb78e38 App redesign (#2946)
* Start of app redesign

* format

* continue progress

* Content page nearly done

* Fix recursion issues with content page

* Fix update all alignment

* Discover page progress

* Settings progress

* Removed unlocked-size hack that breaks web

* Revamp project page, refactor web project page to share code with app, fixed loading bar, misc UI/UX enhancements, update ko-fi logo, update arrow icons, fix web issues caused by floating-vue migration, fix tooltip issues, update web tooltips, clean up web hydration issues

* Ads + run prettier

* Begin auth refactor, move common messages to ui lib, add i18n extraction to all apps, begin Library refactor

* fix ads not hiding when plus log in

* rev lockfile changes/conflicts

* Fix sign in page

* Add generated

* (mostly) Data driven search

* Fix search mobile issue

* profile fixes

* Project versions page, fix typescript on UI lib and misc fixes

* Remove unused gallery component

* Fix linkfunction err

* Search filter controls at top, localization for locked filters

* Fix provided filter names

* Fix navigating from instance browse to main browse

* Friends frontend (#2995)

* Friends system frontend

* (almost) finish frontend

* finish friends, fix lint

* Fix lint

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>

* Refresh macOS app icon

* Update web search UI more

* Fix link opens

* Fix frontend build

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
2024-12-11 19:54:18 -08:00

169 lines
4.4 KiB
Vue

<template>
<nav
ref="scrollContainer"
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
>
<NuxtLink
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
:key="index"
ref="tabLinkElements"
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
:class="{
'text-button-textSelected': activeIndex === index && !subpageSelected,
'text-contrast': activeIndex === index && subpageSelected,
}"
>
<component :is="link.icon" v-if="link.icon" class="size-5" />
<span class="text-nowrap">{{ link.label }}</span>
</NuxtLink>
<div
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'
}`"
:style="{
left: sliderLeftPx,
top: sliderTopPx,
right: sliderRightPx,
bottom: sliderBottomPx,
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
}"
aria-hidden="true"
></div>
</nav>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
const route = useNativeRoute();
interface Tab {
label: string;
href: string;
shown?: boolean;
icon?: string;
subpages?: string[];
}
const props = defineProps<{
links: Tab[];
query?: string;
}>();
const scrollContainer = ref<HTMLElement | null>(null);
const sliderLeft = ref(4);
const sliderTop = ref(4);
const sliderRight = ref(4);
const sliderBottom = ref(4);
const activeIndex = ref(-1);
const subpageSelected = ref(false);
const filteredLinks = computed(() =>
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
);
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
const sliderTopPx = computed(() => `${sliderTop.value}px`);
const sliderRightPx = computed(() => `${sliderRight.value}px`);
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
const tabLinkElements = ref();
function pickLink() {
let index = -1;
subpageSelected.value = false;
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i];
if (decodeURIComponent(route.path) === link.href) {
index = i;
break;
} else if (
decodeURIComponent(route.path).includes(link.href) ||
(link.subpages &&
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
) {
index = i;
subpageSelected.value = true;
break;
}
}
activeIndex.value = index;
if (activeIndex.value !== -1) {
startAnimation();
} else {
sliderLeft.value = 0;
sliderRight.value = 0;
}
}
function startAnimation() {
const el = tabLinkElements.value[activeIndex.value]?.$el;
if (!el || !el.offsetParent) return;
const newValues = {
left: el.offsetLeft,
top: el.offsetTop,
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
};
if (sliderLeft.value === 4 && sliderRight.value === 4) {
sliderLeft.value = newValues.left;
sliderRight.value = newValues.right;
sliderTop.value = newValues.top;
sliderBottom.value = newValues.bottom;
} else {
const delay = 200;
if (newValues.left < sliderLeft.value) {
sliderLeft.value = newValues.left;
setTimeout(() => {
sliderRight.value = newValues.right;
}, delay);
} else {
sliderRight.value = newValues.right;
setTimeout(() => {
sliderLeft.value = newValues.left;
}, delay);
}
if (newValues.top < sliderTop.value) {
sliderTop.value = newValues.top;
setTimeout(() => {
sliderBottom.value = newValues.bottom;
}, delay);
} else {
sliderBottom.value = newValues.bottom;
setTimeout(() => {
sliderTop.value = newValues.top;
}, delay);
}
}
}
onMounted(() => {
pickLink();
});
watch(
() => route.path,
() => pickLink(),
);
</script>
<style scoped>
.navtabs-transition {
transition:
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
}
.card-shadow {
box-shadow: var(--shadow-card);
}
</style>