mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
* fix: files.vue bugs before styling changes
* feat: move files tab to shared layout structure
* fix: qa
* fix: qa
* fix: bugs
* fix: lint
* fix: admonition cleanup with progress + actions
* fix: cleanup
* fix: modals
* fix: admon title
* fix: i18n standard
* fix: lint + i18n pass
* fix: remove transition
* fix: type errors
* feat: files tab in app
* fix: qa
* fix: backup item minmax
* fix: use ContentPageHeader for server panel
* fix: lint
* fix: lint
* fix: lint
* feat: page leave safety
* fix: lint
* fix: cargo fmt fix
* fix: blank in prod
* fix: content card table stuff
* Revert "fix: blank in prod"
This reverts commit 74758fe185.
* fix: import
* feat: browse worlds/servers flow
* fix: worlds tab parity with content tab
* fix: perf bug + shader filter pill copy
* feat: singleplayer filter
* fix: ordering
* fix: breadcrumbs
* fix: lint
* fix: qa
* feat: store server proj id when adding to a non-linked instance
* fix: lint
* fix: i18n + qa
* fix: conflict
* qa: already installed modal + placeholders not server-specific
* fix: qa
* fix: add + edit server modals
* fix: qa
* fix: security
* fix: devin flags
* fix: lint
* chore: change file to break build cache
* fix: admon
* fix: import path stuff
* feat: qa
* fix: fmt fmt idiot
---------
Signed-off-by: Calum H. <calum@modrinth.com>
159 lines
3.7 KiB
Vue
159 lines
3.7 KiB
Vue
<template>
|
|
<div
|
|
ref="outerRef"
|
|
data-tauri-drag-region
|
|
class="min-w-0 overflow-hidden pl-3"
|
|
:class="{ 'breadcrumb-fade-mask': isOverflowing }"
|
|
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
|
|
@mouseenter="onMouseEnter"
|
|
@mouseleave="onMouseLeave"
|
|
>
|
|
<div
|
|
ref="innerRef"
|
|
data-tauri-drag-region
|
|
class="flex w-fit items-center gap-1"
|
|
:class="{ 'breadcrumbs-scroll': isAnimating }"
|
|
@animationiteration="onAnimationIteration"
|
|
>
|
|
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
|
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
|
<router-link
|
|
v-if="breadcrumb.link"
|
|
:to="{
|
|
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
|
|
query: breadcrumb.query,
|
|
}"
|
|
class="shrink-0 whitespace-nowrap text-primary"
|
|
>
|
|
{{ resolveLabel(breadcrumb.name) }}
|
|
</router-link>
|
|
<span
|
|
v-else
|
|
data-tauri-drag-region
|
|
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
|
|
>
|
|
{{ resolveLabel(breadcrumb.name) }}
|
|
</span>
|
|
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ChevronRightIcon } from '@modrinth/assets'
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
|
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
|
|
interface Breadcrumb {
|
|
name: string
|
|
link?: string
|
|
query?: Record<string, string>
|
|
}
|
|
|
|
const route = useRoute()
|
|
const breadcrumbData = useBreadcrumbs()
|
|
|
|
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
|
const additionalContext =
|
|
route.meta.useContext === true
|
|
? breadcrumbData.context
|
|
: route.meta.useRootContext === true
|
|
? breadcrumbData.rootContext
|
|
: null
|
|
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
|
|
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
|
|
})
|
|
|
|
function resolveLabel(name: string): string {
|
|
return name.charAt(0) === '?' ? breadcrumbData.getName(name.slice(1)) : name
|
|
}
|
|
|
|
// Overflow detection
|
|
const outerRef = ref<HTMLDivElement | null>(null)
|
|
const innerRef = ref<HTMLDivElement | null>(null)
|
|
const isOverflowing = ref(false)
|
|
const isAnimating = ref(false)
|
|
const overflowAmount = ref(0)
|
|
|
|
let hovered = false
|
|
let stopping = false
|
|
|
|
function checkOverflow() {
|
|
if (!outerRef.value || !innerRef.value) return
|
|
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
|
|
isOverflowing.value = overflow > 0
|
|
overflowAmount.value = overflow + 12
|
|
}
|
|
|
|
function onMouseEnter() {
|
|
hovered = true
|
|
stopping = false
|
|
if (isOverflowing.value) {
|
|
isAnimating.value = true
|
|
}
|
|
}
|
|
|
|
function onMouseLeave() {
|
|
hovered = false
|
|
if (isAnimating.value) {
|
|
stopping = true
|
|
}
|
|
}
|
|
|
|
function onAnimationIteration() {
|
|
if (stopping && !hovered) {
|
|
isAnimating.value = false
|
|
stopping = false
|
|
}
|
|
}
|
|
|
|
let resizeObserver: ResizeObserver | null = null
|
|
|
|
onMounted(() => {
|
|
checkOverflow()
|
|
resizeObserver = new ResizeObserver(checkOverflow)
|
|
if (outerRef.value) resizeObserver.observe(outerRef.value)
|
|
if (innerRef.value) resizeObserver.observe(innerRef.value)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
resizeObserver?.disconnect()
|
|
})
|
|
|
|
watch(breadcrumbs, () => {
|
|
requestAnimationFrame(checkOverflow)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.breadcrumb-fade-mask {
|
|
mask-image: linear-gradient(
|
|
to right,
|
|
transparent,
|
|
black 12px,
|
|
black calc(100% - 12px),
|
|
transparent
|
|
);
|
|
}
|
|
|
|
.breadcrumbs-scroll {
|
|
animation: breadcrumb-scroll 10s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes breadcrumb-scroll {
|
|
0% {
|
|
transform: translateX(0);
|
|
}
|
|
35%,
|
|
65% {
|
|
transform: translateX(var(--scroll-distance));
|
|
}
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
</style>
|