mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Added <BubbleMenu /> Component
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = ['Prism', 'Plasma', 'Electric Border', 'Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Glass Surface', 'Sticker Peel', 'Scroll Stack', 'Faulty Terminal', 'Pill Nav', 'Card Nav', 'Logo Loop'];
|
export const NEW = ['Prism', 'Plasma', 'Electric Border', 'Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Glass Surface', 'Sticker Peel', 'Scroll Stack', 'Faulty Terminal', 'Pill Nav', 'Card Nav', 'Logo Loop', 'Bubble Menu'];
|
||||||
export const UPDATED = [];
|
export const UPDATED = [];
|
||||||
|
|
||||||
// Used for main sidebar navigation
|
// Used for main sidebar navigation
|
||||||
@@ -72,6 +72,7 @@ export const CATEGORIES = [
|
|||||||
'Pill Nav',
|
'Pill Nav',
|
||||||
'Dock',
|
'Dock',
|
||||||
'Gooey Nav',
|
'Gooey Nav',
|
||||||
|
'Bubble Menu',
|
||||||
'Pixel Card',
|
'Pixel Card',
|
||||||
'Carousel',
|
'Carousel',
|
||||||
'Spotlight Card',
|
'Spotlight Card',
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const components = {
|
|||||||
'counter': () => import('../demo/Components/CounterDemo.vue'),
|
'counter': () => import('../demo/Components/CounterDemo.vue'),
|
||||||
'rolling-gallery': () => import('../demo/Components/RollingGalleryDemo.vue'),
|
'rolling-gallery': () => import('../demo/Components/RollingGalleryDemo.vue'),
|
||||||
'scroll-stack': () => import('../demo/Components/ScrollStackDemo.vue'),
|
'scroll-stack': () => import('../demo/Components/ScrollStackDemo.vue'),
|
||||||
|
'bubble-menu': () => import('../demo/Components/BubbleMenuDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgrounds = {
|
const backgrounds = {
|
||||||
|
|||||||
62
src/constants/code/Components/bubbleMenuCode.ts
Normal file
62
src/constants/code/Components/bubbleMenuCode.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import code from '@content/Components/BubbleMenu/BubbleMenu.vue?raw';
|
||||||
|
import { createCodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const bubbleMenu = createCodeObject(code, 'Components/BubbleMenu', {
|
||||||
|
installation: `npm install gsap`,
|
||||||
|
usage: `<template>
|
||||||
|
<BubbleMenu
|
||||||
|
:logo="() => h('span', { style: { fontWeight: 700 } }, 'VB')"
|
||||||
|
:items="items"
|
||||||
|
menu-aria-label="Toggle navigation"
|
||||||
|
menu-bg="#ffffff"
|
||||||
|
menu-content-color="#111111"
|
||||||
|
:use-fixed-position="false"
|
||||||
|
animation-ease="back.out(1.5)"
|
||||||
|
:animation-duration="0.5"
|
||||||
|
:stagger-delay="0.12"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BubbleMenu from './BubbleMenu.vue'
|
||||||
|
import { h } from 'vue'
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'home',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Home',
|
||||||
|
rotation: -8,
|
||||||
|
hoverStyles: { bgColor: '#3b82f6', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'about',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'About',
|
||||||
|
rotation: 8,
|
||||||
|
hoverStyles: { bgColor: '#10b981', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'projects',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Projects',
|
||||||
|
rotation: 8,
|
||||||
|
hoverStyles: { bgColor: '#f59e0b', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'blog',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Blog',
|
||||||
|
rotation: 8,
|
||||||
|
hoverStyles: { bgColor: '#ef4444', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'contact',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Contact',
|
||||||
|
rotation: -8,
|
||||||
|
hoverStyles: { bgColor: '#8b5cf6', textColor: '#ffffff' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
443
src/content/Components/BubbleMenu/BubbleMenu.vue
Normal file
443
src/content/Components/BubbleMenu/BubbleMenu.vue
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { gsap } from 'gsap';
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
useTemplateRef,
|
||||||
|
watch,
|
||||||
|
type CSSProperties,
|
||||||
|
type VNode
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
type MenuItem = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
rotation?: number;
|
||||||
|
hoverStyles?: {
|
||||||
|
bgColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BubbleMenuProps = {
|
||||||
|
logo: string | VNode;
|
||||||
|
onMenuClick?: (open: boolean) => void;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
menuAriaLabel?: string;
|
||||||
|
menuBg?: string;
|
||||||
|
menuContentColor?: string;
|
||||||
|
useFixedPosition?: boolean;
|
||||||
|
items?: MenuItem[];
|
||||||
|
animationEase?: string;
|
||||||
|
animationDuration?: number;
|
||||||
|
staggerDelay?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_ITEMS: MenuItem[] = [
|
||||||
|
{
|
||||||
|
label: 'home',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Home',
|
||||||
|
rotation: -8,
|
||||||
|
hoverStyles: { bgColor: '#3b82f6', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'about',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'About',
|
||||||
|
rotation: 8,
|
||||||
|
hoverStyles: { bgColor: '#10b981', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'projects',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Documentation',
|
||||||
|
rotation: 8,
|
||||||
|
hoverStyles: { bgColor: '#f59e0b', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'blog',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Blog',
|
||||||
|
rotation: 8,
|
||||||
|
hoverStyles: { bgColor: '#ef4444', textColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'contact',
|
||||||
|
href: '#',
|
||||||
|
ariaLabel: 'Contact',
|
||||||
|
rotation: -8,
|
||||||
|
hoverStyles: { bgColor: '#8b5cf6', textColor: '#ffffff' }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<BubbleMenuProps>(), {
|
||||||
|
menuAriaLabel: 'Toggle menu',
|
||||||
|
menuBg: '#fff',
|
||||||
|
menuContentColor: '#111',
|
||||||
|
useFixedPosition: false,
|
||||||
|
animationEase: 'back.out(1.5)',
|
||||||
|
animationDuration: 0.5,
|
||||||
|
staggerDelay: 0.12
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMenuOpen = ref(false);
|
||||||
|
const showOverlay = ref(false);
|
||||||
|
|
||||||
|
const overlayRef = useTemplateRef('overlayRef');
|
||||||
|
const bubblesRef = ref<HTMLAnchorElement[]>([]);
|
||||||
|
const labelRefs = ref<HTMLSpanElement[]>([]);
|
||||||
|
|
||||||
|
const setBubbleRef = (el: HTMLAnchorElement | null, idx: number) => {
|
||||||
|
if (el) {
|
||||||
|
bubblesRef.value[idx] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLabelsRef = (el: HTMLSpanElement | null, idx: number) => {
|
||||||
|
if (el) {
|
||||||
|
labelRefs.value[idx] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuItems = computed(() => (props.items?.length ? props.items : DEFAULT_ITEMS));
|
||||||
|
|
||||||
|
const containerClassName = computed(() =>
|
||||||
|
[
|
||||||
|
'bubble-menu',
|
||||||
|
props.useFixedPosition ? 'fixed' : 'absolute',
|
||||||
|
'left-0 right-0 top-8',
|
||||||
|
'flex items-center justify-between',
|
||||||
|
'gap-4 px-8',
|
||||||
|
'pointer-events-none',
|
||||||
|
'z-[1001]',
|
||||||
|
props.className
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
const nextState = !isMenuOpen.value;
|
||||||
|
if (nextState) showOverlay.value = true;
|
||||||
|
isMenuOpen.value = nextState;
|
||||||
|
props.onMenuClick?.(nextState);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [isMenuOpen.value, showOverlay.value, props.animationEase, props.animationDuration, props.staggerDelay],
|
||||||
|
async () => {
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const overlay = overlayRef.value;
|
||||||
|
const bubbles = bubblesRef.value.filter(Boolean);
|
||||||
|
const labels = labelRefs.value.filter(Boolean);
|
||||||
|
if (!overlay || !bubbles.length) return;
|
||||||
|
|
||||||
|
if (isMenuOpen.value) {
|
||||||
|
gsap.set(overlay, { display: 'flex' });
|
||||||
|
gsap.killTweensOf([...bubbles, ...labels]);
|
||||||
|
gsap.set(bubbles, { scale: 0, transformOrigin: '50% 50%' });
|
||||||
|
gsap.set(labels, { y: 24, autoAlpha: 0 });
|
||||||
|
|
||||||
|
bubbles.forEach((bubble, i) => {
|
||||||
|
const delay = i * props.staggerDelay + gsap.utils.random(-0.05, 0.05);
|
||||||
|
const tl = gsap.timeline({ delay });
|
||||||
|
tl.to(bubble, {
|
||||||
|
scale: 1,
|
||||||
|
duration: props.animationDuration,
|
||||||
|
ease: props.animationEase
|
||||||
|
});
|
||||||
|
if (labels[i]) {
|
||||||
|
tl.to(
|
||||||
|
labels[i],
|
||||||
|
{
|
||||||
|
y: 0,
|
||||||
|
autoAlpha: 1,
|
||||||
|
duration: props.animationDuration,
|
||||||
|
ease: 'power3.out'
|
||||||
|
},
|
||||||
|
'-=' + props.animationDuration * 0.9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (showOverlay.value) {
|
||||||
|
gsap.killTweensOf([...bubbles, ...labels]);
|
||||||
|
gsap.to(labels, {
|
||||||
|
y: 24,
|
||||||
|
autoAlpha: 0,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: 'power3.in'
|
||||||
|
});
|
||||||
|
gsap.to(bubbles, {
|
||||||
|
scale: 0,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: 'power3.in',
|
||||||
|
onComplete: () => {
|
||||||
|
gsap.set(overlay, { display: 'none' });
|
||||||
|
showOverlay.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
let cleanupResizeListener: () => void;
|
||||||
|
const handleResize = () => {
|
||||||
|
if (isMenuOpen.value) {
|
||||||
|
const bubbles = bubblesRef.value.filter(Boolean);
|
||||||
|
const isDesktop = window.innerWidth >= 900;
|
||||||
|
bubbles.forEach((bubble, i) => {
|
||||||
|
const item = menuItems.value[i];
|
||||||
|
if (bubble && item) {
|
||||||
|
const rotation = isDesktop ? (item.rotation ?? 0) : 0;
|
||||||
|
gsap.set(bubble, { rotation });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
cleanupResizeListener = () => window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanupResizeListener?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [isMenuOpen.value, menuItems.value],
|
||||||
|
() => {
|
||||||
|
cleanupResizeListener?.();
|
||||||
|
handleResize();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav :class="containerClassName" :style="style" aria-label="Main navigation">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'bubble logo-bubble',
|
||||||
|
'inline-flex items-center justify-center',
|
||||||
|
'rounded-full',
|
||||||
|
'bg-white',
|
||||||
|
'shadow-[0_4px_16px_rgba(0,0,0,0.12)]',
|
||||||
|
'pointer-events-auto',
|
||||||
|
'h-12 md:h-14',
|
||||||
|
'px-4 md:px-8',
|
||||||
|
'gap-2',
|
||||||
|
'will-change-transform'
|
||||||
|
]"
|
||||||
|
aria-label="Logo"
|
||||||
|
:style="{
|
||||||
|
background: menuBg,
|
||||||
|
minHeight: '48px',
|
||||||
|
borderRadius: '9999px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="['logo-content', 'inline-flex items-center justify-center', 'w-[120px] h-full']"
|
||||||
|
:style="{ '--logo-max-height': '60%', '--logo-max-width': '100%' }"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="typeof logo === 'string'"
|
||||||
|
:src="logo as string"
|
||||||
|
alt="Logo"
|
||||||
|
class="block max-w-full max-h-[60%] object-contain bubble-logo"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
<component :is="logo" />
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="[
|
||||||
|
'bubble toggle-bubble menu-btn',
|
||||||
|
isMenuOpen ? 'open' : '',
|
||||||
|
'inline-flex flex-col items-center justify-center',
|
||||||
|
'rounded-full',
|
||||||
|
'bg-white',
|
||||||
|
'shadow-[0_4px_16px_rgba(0,0,0,0.12)]',
|
||||||
|
'pointer-events-auto',
|
||||||
|
'w-12 h-12 md:w-14 md:h-14',
|
||||||
|
'border-0 cursor-pointer p-0',
|
||||||
|
'will-change-transform'
|
||||||
|
]"
|
||||||
|
@click="handleToggle"
|
||||||
|
:aria-label="menuAriaLabel"
|
||||||
|
:aria-pressed="isMenuOpen"
|
||||||
|
:style="{ background: menuBg }"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block mx-auto rounded-[2px] menu-line"
|
||||||
|
:style="{
|
||||||
|
width: '26px',
|
||||||
|
height: '2px',
|
||||||
|
background: menuContentColor,
|
||||||
|
transform: isMenuOpen ? 'translateY(4px) rotate(45deg)' : 'none'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="block mx-auto rounded-[2px] menu-line short"
|
||||||
|
:style="{
|
||||||
|
marginTop: '6px',
|
||||||
|
width: '26px',
|
||||||
|
height: '2px',
|
||||||
|
background: menuContentColor,
|
||||||
|
transform: isMenuOpen ? 'translateY(-4px) rotate(-45deg)' : 'none'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showOverlay"
|
||||||
|
ref="overlayRef"
|
||||||
|
:class="[
|
||||||
|
'bubble-menu-items',
|
||||||
|
useFixedPosition ? 'fixed' : 'absolute',
|
||||||
|
'inset-0',
|
||||||
|
'flex items-center justify-center',
|
||||||
|
'pointer-events-none',
|
||||||
|
'z-[1000]'
|
||||||
|
]"
|
||||||
|
:aria-hidden="!isMenuOpen"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
:class="[
|
||||||
|
'pill-list',
|
||||||
|
'list-none m-0 px-6',
|
||||||
|
'w-full max-w-[1600px] mx-auto',
|
||||||
|
'flex flex-wrap',
|
||||||
|
'gap-x-0 gap-y-1',
|
||||||
|
'pointer-events-auto'
|
||||||
|
]"
|
||||||
|
role="menu"
|
||||||
|
aria-label="Menu links"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(item, idx) in menuItems"
|
||||||
|
:key="idx"
|
||||||
|
role="none"
|
||||||
|
:class="['pill-col', 'flex justify-center items-stretch', '[flex:0_0_calc(100%/3)]', 'box-border']"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
role="menuitem"
|
||||||
|
:href="item.href"
|
||||||
|
:aria-label="item.ariaLabel || item.label"
|
||||||
|
:class="[
|
||||||
|
'pill-link',
|
||||||
|
'w-full',
|
||||||
|
'rounded-[999px]',
|
||||||
|
'no-underline',
|
||||||
|
'bg-white',
|
||||||
|
'text-inherit',
|
||||||
|
'shadow-[0_4px_14px_rgba(0,0,0,0.10)]',
|
||||||
|
'flex items-center justify-center',
|
||||||
|
'relative',
|
||||||
|
'transition-[background,color] duration-300 ease-in-out',
|
||||||
|
'box-border',
|
||||||
|
'whitespace-nowrap overflow-hidden'
|
||||||
|
]"
|
||||||
|
:style="{
|
||||||
|
'--item-rot': `${item.rotation ?? 0}deg`,
|
||||||
|
'--pill-bg': menuBg,
|
||||||
|
'--pill-color': menuContentColor,
|
||||||
|
'--hover-bg': item.hoverStyles?.bgColor || '#f3f4f6',
|
||||||
|
'--hover-color': item.hoverStyles?.textColor || menuContentColor,
|
||||||
|
background: 'var(--pill-bg)',
|
||||||
|
color: 'var(--pill-color)',
|
||||||
|
minHeight: 'var(--pill-min-h, 160px)',
|
||||||
|
padding: 'clamp(1.5rem, 3vw, 8rem) 0',
|
||||||
|
fontSize: 'clamp(1.5rem, 4vw, 4rem)',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 0,
|
||||||
|
willChange: 'transform',
|
||||||
|
height: '10px'
|
||||||
|
}"
|
||||||
|
:ref="el => setBubbleRef(el as HTMLAnchorElement | null, idx)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-block pill-label"
|
||||||
|
:style="{
|
||||||
|
willChange: 'transform, opacity',
|
||||||
|
height: '1.2em',
|
||||||
|
lineHeight: 1.2
|
||||||
|
}"
|
||||||
|
:ref="el => setLabelsRef(el as HTMLSpanElement | null, idx)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bubble-menu .menu-line {
|
||||||
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
opacity 0.3s ease;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-list .pill-col:nth-child(4):nth-last-child(2) {
|
||||||
|
margin-left: calc(100% / 6);
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-list .pill-col:nth-child(4):last-child {
|
||||||
|
margin-left: calc(100% / 3);
|
||||||
|
}
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.bubble-menu-items .pill-link {
|
||||||
|
transform: rotate(var(--item-rot));
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-link:hover {
|
||||||
|
transform: rotate(var(--item-rot)) scale(1.06);
|
||||||
|
background: var(--hover-bg) !important;
|
||||||
|
color: var(--hover-color) !important;
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-link:active {
|
||||||
|
transform: rotate(var(--item-rot)) scale(0.94);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.bubble-menu-items {
|
||||||
|
padding-top: 120px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-list {
|
||||||
|
row-gap: 16px;
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-list .pill-col {
|
||||||
|
flex: 0 0 100% !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-link {
|
||||||
|
font-size: clamp(1.2rem, 3vw, 4rem);
|
||||||
|
padding: clamp(1rem, 2vw, 2rem) 0;
|
||||||
|
min-height: 80px !important;
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-link:hover {
|
||||||
|
transform: scale(1.06);
|
||||||
|
background: var(--hover-bg);
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
.bubble-menu-items .pill-link:active {
|
||||||
|
transform: scale(0.94);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
143
src/demo/Components/BubbleMenuDemo.vue
Normal file
143
src/demo/Components/BubbleMenuDemo.vue
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="relative h-[800px] overflow-hidden demo-container demo-container-dots">
|
||||||
|
<BubbleMenu
|
||||||
|
:logo="logo"
|
||||||
|
:menu-bg="menuBg"
|
||||||
|
:menu-content-color="menuContentColor"
|
||||||
|
:animation-ease="animationEase"
|
||||||
|
:animation-duration="animationDuration"
|
||||||
|
:stagger-delay="staggerDelay"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSelect title="Ease" :options="easeOptions" v-model="animationEase" />
|
||||||
|
<PreviewColor title="Menu BG" v-model="menuBg" />
|
||||||
|
<PreviewColor title="Content Color" v-model="menuContentColor" />
|
||||||
|
<PreviewSlider title="Anim Duration" v-model="animationDuration" :min="0.1" :max="2" :step="0.05" />
|
||||||
|
<PreviewSlider title="Stagger" v-model="staggerDelay" :min="0" :max="0.5" :step="0.01" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['gsap']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="bubbleMenu" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="bubbleMenu.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { bubbleMenu } from '@/constants/code/Components/bubbleMenuCode';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import logo from '../../assets/logos/vuebits-gh-black.svg';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue';
|
||||||
|
import Customize from '../../components/common/Customize.vue';
|
||||||
|
import PreviewColor from '../../components/common/PreviewColor.vue';
|
||||||
|
import PreviewSelect from '../../components/common/PreviewSelect.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import BubbleMenu from '../../content/Components/BubbleMenu/BubbleMenu.vue';
|
||||||
|
|
||||||
|
type EaseKey = 'back.out(1.5)' | 'power3.out' | 'power2.out' | 'elastic.out(1,0.5)' | 'bounce.out';
|
||||||
|
|
||||||
|
const animationEase = ref<EaseKey>('back.out(1.5)');
|
||||||
|
const menuBg = ref('#ffffff');
|
||||||
|
const menuContentColor = ref('#111111');
|
||||||
|
const animationDuration = ref(0.5);
|
||||||
|
const staggerDelay = ref(0.12);
|
||||||
|
|
||||||
|
const easeOptions = [
|
||||||
|
{ value: 'back.out(1.5)', label: 'back.out(1.5)' },
|
||||||
|
{ value: 'power3.out', label: 'power3.out' },
|
||||||
|
{ value: 'power2.out', label: 'power2.out' },
|
||||||
|
{ value: 'elastic.out(1,0.5)', label: 'elastic.out(1,0.5)' },
|
||||||
|
{ value: 'bounce.out', label: 'bounce.out' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'logo',
|
||||||
|
type: 'ReactNode | string',
|
||||||
|
default: '—',
|
||||||
|
description: 'Logo content shown in the central bubble (string src or JSX).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onMenuClick',
|
||||||
|
type: '(open: boolean) => void',
|
||||||
|
default: '—',
|
||||||
|
description: 'Callback fired whenever the menu toggle changes; receives open state.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: '—',
|
||||||
|
description: 'Additional class names for the root nav wrapper.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'style',
|
||||||
|
type: 'CSSProperties',
|
||||||
|
default: '—',
|
||||||
|
description: 'Inline styles applied to the root nav wrapper.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'menuAriaLabel',
|
||||||
|
type: 'string',
|
||||||
|
default: '"Toggle menu"',
|
||||||
|
description: 'Accessible aria-label for the toggle button.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'menuBg',
|
||||||
|
type: 'string',
|
||||||
|
default: '"#fff"',
|
||||||
|
description: 'Background color for the logo & toggle bubbles and base pill background.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'menuContentColor',
|
||||||
|
type: 'string',
|
||||||
|
default: '"#111"',
|
||||||
|
description: 'Color for the menu icon lines and default pill text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useFixedPosition',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'If true positions the menu with fixed instead of absolute (follows viewport).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'MenuItem[]',
|
||||||
|
default: 'DEFAULT_ITEMS',
|
||||||
|
description:
|
||||||
|
'Custom menu items; each = { label, href, ariaLabel?, rotation?, hoverStyles?: { bgColor?, textColor? } }.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'animationEase',
|
||||||
|
type: 'string',
|
||||||
|
default: '"back.out(1.5)"',
|
||||||
|
description: 'GSAP ease string used for bubble scale-in animation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'animationDuration',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.5',
|
||||||
|
description: 'Duration (s) for each bubble & label animation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'staggerDelay',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.12',
|
||||||
|
description: 'Base stagger (s) between bubble animations (with slight random variance).'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user