diff --git a/packages/ui-transitions/playground/src/components/Buttons.vue b/packages/ui-transitions/playground/src/components/Buttons.vue index a016e2c74..0403dbc41 100644 --- a/packages/ui-transitions/playground/src/components/Buttons.vue +++ b/packages/ui-transitions/playground/src/components/Buttons.vue @@ -1,46 +1,26 @@ diff --git a/packages/ui-transitions/playground/src/pages/transition-7.vue b/packages/ui-transitions/playground/src/pages/transition-7.vue new file mode 100644 index 000000000..795c89080 --- /dev/null +++ b/packages/ui-transitions/playground/src/pages/transition-7.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/ui-transitions/src/components/BubbleWaveOutTransition.vue b/packages/ui-transitions/src/components/BubbleWaveOutTransition.vue new file mode 100644 index 000000000..a7ca44a19 --- /dev/null +++ b/packages/ui-transitions/src/components/BubbleWaveOutTransition.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/packages/ui-transitions/src/components/FantasyFallTransition.vue b/packages/ui-transitions/src/components/FantasyFallTransition.vue index 66044c19a..6c862bd84 100644 --- a/packages/ui-transitions/src/components/FantasyFallTransition.vue +++ b/packages/ui-transitions/src/components/FantasyFallTransition.vue @@ -5,19 +5,29 @@ const props = withDefaults(defineProps<{ primaryColor?: string duration?: number delay?: number - borderRadius?: number + borderRadius?: { + sm?: string + md?: string + lg?: string + } }>(), { primaryColor: '#eee', duration: 0.6, delay: 0, - borderRadius: 50, + borderRadius: () => ({ + sm: '14rem', + md: '14rem', + lg: '50%', + }), }) onMounted(() => { document.documentElement.style.setProperty('--fantasy-fall-color', props.primaryColor) document.documentElement.style.setProperty('--fantasy-fall-duration', `${props.duration}s`) document.documentElement.style.setProperty('--fantasy-fall-delay', `${props.delay}s`) - document.documentElement.style.setProperty('--fantasy-fall-radius', `${props.borderRadius}%`) + document.documentElement.style.setProperty('--fantasy-fall-radius-sm', `${props.borderRadius.sm}`) + document.documentElement.style.setProperty('--fantasy-fall-radius-md', `${props.borderRadius.md}`) + document.documentElement.style.setProperty('--fantasy-fall-radius-lg', `${props.borderRadius.lg}`) }) @@ -41,8 +51,8 @@ onMounted(() => { height: 100%; background-color: var(--fantasy-fall-color); transform: translateY(-100%); - border-bottom-left-radius: var(--fantasy-fall-radius); - border-bottom-right-radius: var(--fantasy-fall-radius); + border-bottom-left-radius: var(--fantasy-fall-radius-sm); + border-bottom-right-radius: var(--fantasy-fall-radius-sm); animation: fantasy-fall var(--fantasy-fall-duration) ease-out var(--fantasy-fall-delay) forwards; } } @@ -62,4 +72,22 @@ onMounted(() => { border-bottom-left-radius: 0%; } } + +@media (min-width: 768px) { + .fantasy-fall-transition { + &::before { + border-bottom-left-radius: var(--fantasy-fall-radius-md); + border-bottom-right-radius: var(--fantasy-fall-radius-md); + } + } +} + +@media (min-width: 1024px) { + .fantasy-fall-transition { + &::before { + border-bottom-left-radius: var(--fantasy-fall-radius-lg); + border-bottom-right-radius: var(--fantasy-fall-radius-lg); + } + } +} diff --git a/packages/ui-transitions/src/components/StageTransitionGroup.vue b/packages/ui-transitions/src/components/StageTransitionGroup.vue index 12e172d0c..bba899db8 100644 --- a/packages/ui-transitions/src/components/StageTransitionGroup.vue +++ b/packages/ui-transitions/src/components/StageTransitionGroup.vue @@ -3,6 +3,7 @@ import { ref, shallowRef } from 'vue' import { useRouter } from 'vue-router' import ArrowTransition from './ArrowTransition.vue' +import BubbleWaveOutTransition from './BubbleWaveOutTransition.vue' import FantasyFallTransition from './FantasyFallTransition.vue' import MultipleBlocksRevealTransition from './MultipleBlocksRevealTransition.vue' import RectanglesRotateTransition from './RectanglesRotateTransition.vue' @@ -24,15 +25,35 @@ type TransitionComponent = | typeof MultipleBlocksRevealTransition | typeof FantasyFallTransition | typeof RectanglesRotateTransition + | typeof BubbleWaveOutTransition const router = useRouter() const showTransition = ref(false) const activeTransitionName = ref('') +const transitionStage = ref(null) -const transitions = shallowRef void | Promise +type NavigationCallback = () => void + +interface TransitionOptions { component: TransitionComponent duration: number -}>>({ + exitDuration?: number + nextDelay?: number +} + +const transitions = shallowRef>({ 'slide': { component: SlideTransition, duration: 2700, @@ -57,47 +78,154 @@ const transitions = shallowRef((resolve) => { - const transition = transitions.value[name] - if (!transition) { - console.error(`Transition ${name} not found`) - resolve() - return - } +// Hook system +const lifecycleHooks = ref([]) +// Add a hook +function addTransitionHook(hook: TransitionHook): () => void { + lifecycleHooks.value.push(hook) + + // Return function to remove the hook + return () => { + const index = lifecycleHooks.value.indexOf(hook) + if (index >= 0) { + lifecycleHooks.value.splice(index, 1) + } + } +} + +// Trigger all hooks for a stage +async function triggerHooks(stage: TransitionStage, data: any = {}) { + transitionStage.value = stage + + // Execute all hooks and wait for them to complete + for (const hook of lifecycleHooks.value) { + try { + await Promise.resolve(hook(stage, data)) + } + catch (error) { + console.error(`Error in transition hook at stage "${stage}":`, error) + } + } +} + +async function triggerTransitionAsyncFn(name: string, next: NavigationCallback, resolve: (value: void | PromiseLike) => void) { + const transition = transitions.value[name] + if (!transition) { + console.error(`Transition ${name} not found`) + next() + resolve() + return + } + + // Get navigation timing from configuration, with fallback + const navTiming = transition.nextDelay !== undefined + ? transition.nextDelay + : transition.duration / 3 // Fallback, but configurable + + let hasNavigated = false + + // Hook specifically for determining when to navigate + const navigationHook = (stage: TransitionStage) => { + if (stage === 'navigation' && !hasNavigated) { + hasNavigated = true + next() + } + } + + // Add this hook temporarily + const removeNavHook = addTransitionHook(navigationHook) + + try { + // Before enter + await triggerHooks('before-enter', { transitionName: name }) + + // Handle resetting current transition if needed if (showTransition.value) { + // Fast track to exit if a transition is already active + await triggerHooks('before-leave', { transitionName: activeTransitionName.value }) activeTransitionName.value = '' showTransition.value = false + await triggerHooks('after-leave', { transitionName: activeTransitionName.value }) - setTimeout(() => { - activeTransitionName.value = name - showTransition.value = true - - setTimeout(() => { - showTransition.value = false - activeTransitionName.value = '' - resolve() - }, transition.duration) - }, 50) + // Short delay before starting new transition + await new Promise(r => setTimeout(r, 50)) } - else { - activeTransitionName.value = name - showTransition.value = true - setTimeout(() => { - showTransition.value = false - activeTransitionName.value = '' - resolve() - }, transition.duration) - } + // Start new transition + activeTransitionName.value = name + showTransition.value = true + + // Enter active phase + await triggerHooks('enter-active', { transitionName: name }) + + // Trigger navigation phase at the configured time + setTimeout(async () => { + await triggerHooks('navigation', { + transitionName: name, + config: transition, // Pass full config to hooks + }) + + // Ensure navigation happens even if no hook handled it + if (!hasNavigated) { + hasNavigated = true + next() + } + }, navTiming) + + // After enter - entry phase complete + setTimeout(async () => { + await triggerHooks('after-enter', { transitionName: name }) + }, transition.duration) + + // Before leave - start exit phase + setTimeout(async () => { + await triggerHooks('before-leave', { transitionName: name }) + }, transition.duration + 10) // Small offset after enter completes + + // Leave active - exit animation running + setTimeout(async () => { + await triggerHooks('leave-active', { transitionName: name }) + }, transition.duration + 20) // Small offset + + // After leave - completely finished + const totalDuration = transition.exitDuration ?? 0 + setTimeout(async () => { + showTransition.value = false + activeTransitionName.value = '' + await triggerHooks('after-leave', { transitionName: name }) + resolve() + }, transition.duration + totalDuration) + } + finally { + // Always remove the navigation hook + removeNavHook() + + // Safety - ensure navigation happens no matter what + setTimeout(() => { + if (!hasNavigated) { + hasNavigated = true + next() + } + }, transition.duration * 2) // Conservative timeout + } +} + +// Improved transition trigger with navigation callback +function triggerTransition(name: string, next: NavigationCallback) { + return new Promise((resolve) => { + triggerTransitionAsyncFn(name, next, resolve) }) } router.beforeEach((to, _from, next) => { - triggerTransition(to.meta.stageTransition as string).then(next) + triggerTransition(to.meta.stageTransition as string, next) }) diff --git a/packages/ui-transitions/src/components/index.ts b/packages/ui-transitions/src/components/index.ts index c6162fd5a..2fb20b2a3 100644 --- a/packages/ui-transitions/src/components/index.ts +++ b/packages/ui-transitions/src/components/index.ts @@ -1,4 +1,5 @@ export { default as ArrowTransition } from './ArrowTransition.vue' +export { default as CircleExpansionTransition } from './CircleExpansionTransition.vue' export { default as FantasyFallTransition } from './FantasyFallTransition.vue' export { default as MultipleBlocksRevealTransition } from './MultipleBlocksRevealTransition.vue' export { default as SlideTransition } from './SlideTransition.vue'