Files
Modrinth-plus/packages/ui/src/components/base/ReadyTransition.vue
Calum H. 176d4301c3 feat: shared loading state + cleanup loading state management (#5835)
* feat: implement shared loading bar component and polished loading states across the app

* feat: align loading states + ensureQueryData changes

* fix: lint + bugs

* fix: skeleton for manage servers page

* fix: merge conflict fix
2026-04-18 18:46:39 +00:00

99 lines
2.3 KiB
Vue

<script setup lang="ts">
/**
* If `pending` is false on mount and never becomes true, the slot renders with no
* enter transition (cache-hit fast path). After a real pending phase, transitions
* behave as before for subsequent toggles.
*/
import type { Ref } from 'vue'
import { computed, onBeforeUnmount, ref, toRef, watch } from 'vue'
import { injectLoadingState } from '#ui/providers/loading-state'
const props = withDefaults(
defineProps<{
/** True while the wrapped content is still loading. Slot stays blank, loading bar runs. */
pending: boolean | Ref<boolean>
/** Fade duration applied to the slot when content reveals. */
duration?: number
/** When true, do NOT register a token with the global loading bar — only fade locally. */
silent?: boolean
}>(),
{
duration: 200,
silent: false,
},
)
const pendingRef = toRef(props, 'pending') as Ref<boolean | Ref<boolean>>
const resolvedPending = computed(() => {
const v = pendingRef.value
if (typeof v === 'boolean') return v
return Boolean((v as Ref<boolean>).value)
})
const hasBeenPending = ref(false)
const useShell = computed(() => resolvedPending.value || hasBeenPending.value)
const loadingState = injectLoadingState(null)
let token: symbol | null = null
function release() {
if (token && loadingState) {
loadingState.end(token)
}
token = null
}
watch(
resolvedPending,
(now) => {
if (now) {
hasBeenPending.value = true
}
if (loadingState && !props.silent && typeof window !== 'undefined') {
if (now) {
if (!token) token = loadingState.begin()
} else {
release()
}
}
},
{ immediate: true },
)
onBeforeUnmount(release)
</script>
<template>
<template v-if="useShell">
<Transition name="ready-fade" mode="out-in" :duration="props.duration">
<div v-if="!resolvedPending" key="content" class="ready-transition-content">
<slot />
</div>
<div v-else key="pending" aria-hidden="true" class="ready-transition-pending" />
</Transition>
</template>
<slot v-else />
</template>
<style scoped>
.ready-fade-enter-active,
.ready-fade-leave-active {
transition: opacity v-bind('`${props.duration}ms`') ease-in-out;
}
.ready-fade-enter-from,
.ready-fade-leave-to {
opacity: 0;
}
.ready-transition-content {
width: 100%;
}
.ready-transition-pending {
width: 100%;
height: 100%;
}
</style>