mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Merge pull request #70 from Utkarsh-Singhal-26/feat/pillnav
Added <PillNav /> Component
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel', 'Scroll Stack', 'Faulty Terminal'];
|
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel', 'Scroll Stack', 'Faulty Terminal', 'Pill Nav'];
|
||||||
export const UPDATED = [];
|
export const UPDATED = [];
|
||||||
|
|
||||||
// Used for main sidebar navigation
|
// Used for main sidebar navigation
|
||||||
@@ -66,6 +66,7 @@ export const CATEGORIES = [
|
|||||||
'Magic Bento',
|
'Magic Bento',
|
||||||
'Scroll Stack',
|
'Scroll Stack',
|
||||||
'Profile Card',
|
'Profile Card',
|
||||||
|
'Pill Nav',
|
||||||
'Dock',
|
'Dock',
|
||||||
'Gooey Nav',
|
'Gooey Nav',
|
||||||
'Pixel Card',
|
'Pixel Card',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const components = {
|
|||||||
'magic-bento': () => import('../demo/Components/MagicBentoDemo.vue'),
|
'magic-bento': () => import('../demo/Components/MagicBentoDemo.vue'),
|
||||||
'profile-card': () => import('../demo/Components/ProfileCardDemo.vue'),
|
'profile-card': () => import('../demo/Components/ProfileCardDemo.vue'),
|
||||||
'dock': () => import('../demo/Components/DockDemo.vue'),
|
'dock': () => import('../demo/Components/DockDemo.vue'),
|
||||||
|
'pill-nav': () => import('../demo/Components/PillNavDemo.vue'),
|
||||||
'gooey-nav': () => import('../demo/Components/GooeyNavDemo.vue'),
|
'gooey-nav': () => import('../demo/Components/GooeyNavDemo.vue'),
|
||||||
'pixel-card': () => import('../demo/Components/PixelCardDemo.vue'),
|
'pixel-card': () => import('../demo/Components/PixelCardDemo.vue'),
|
||||||
'carousel': () => import('../demo/Components/CarouselDemo.vue'),
|
'carousel': () => import('../demo/Components/CarouselDemo.vue'),
|
||||||
|
|||||||
30
src/constants/code/Components/pillNavCode.ts
Normal file
30
src/constants/code/Components/pillNavCode.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import code from '@content/Components/PillNav/PillNav.vue?raw';
|
||||||
|
import { createCodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const pillNav = createCodeObject(code, 'Components/PillNav', {
|
||||||
|
installation: `npm install gsap`,
|
||||||
|
usage: `<template>
|
||||||
|
<PillNav
|
||||||
|
:logo="logo"
|
||||||
|
logoAlt="Company Logo"
|
||||||
|
:items="[
|
||||||
|
{ label: 'Home', href: '/' },
|
||||||
|
{ label: 'About', href: '/about' },
|
||||||
|
{ label: 'Services', href: '/services' },
|
||||||
|
{ label: 'Contact', href: '/contact' }
|
||||||
|
]"
|
||||||
|
activeHref="/"
|
||||||
|
class="custom-nav"
|
||||||
|
ease="power2.easeOut"
|
||||||
|
baseColor="#000000"
|
||||||
|
pillColor="#ffffff"
|
||||||
|
hoveredPillTextColor="#ffffff"
|
||||||
|
pillTextColor="#000000"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PillNav from "./PillNav.vue";
|
||||||
|
import logo from '/path/to/logo.svg';
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
416
src/content/Components/PillNav/PillNav.vue
Normal file
416
src/content/Components/PillNav/PillNav.vue
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { gsap } from 'gsap';
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
type PillNavItem = {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PillNavProps {
|
||||||
|
logo: string;
|
||||||
|
logoAlt?: string;
|
||||||
|
items: PillNavItem[];
|
||||||
|
activeHref?: string;
|
||||||
|
className?: string;
|
||||||
|
ease?: string;
|
||||||
|
baseColor?: string;
|
||||||
|
pillColor?: string;
|
||||||
|
hoveredPillTextColor?: string;
|
||||||
|
pillTextColor?: string;
|
||||||
|
onMobileMenuClick?: () => void;
|
||||||
|
initialLoadAnimation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PillNavProps>(), {
|
||||||
|
logoAlt: 'Logo',
|
||||||
|
className: '',
|
||||||
|
ease: 'power3.easeOut',
|
||||||
|
baseColor: '#fff',
|
||||||
|
pillColor: '#060010',
|
||||||
|
hoveredPillTextColor: '#060010',
|
||||||
|
initialLoadAnimation: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedPillTextColor = props.pillTextColor ?? props.baseColor;
|
||||||
|
const isMobileMenuOpen = ref(false);
|
||||||
|
const circleRefs = ref<Array<HTMLSpanElement | null>>([]);
|
||||||
|
const tlRefs = ref<Array<gsap.core.Timeline | null>>([]);
|
||||||
|
const activeTweenRefs = ref<Array<gsap.core.Tween | null>>([]);
|
||||||
|
const logoImgRef = useTemplateRef('logoImgRef');
|
||||||
|
const logoTweenRef = ref<gsap.core.Tween | null>(null);
|
||||||
|
const hamburgerRef = useTemplateRef('hamburgerRef');
|
||||||
|
const mobileMenuRef = useTemplateRef('mobileMenuRef');
|
||||||
|
const navItemsRef = useTemplateRef('navItemsRef');
|
||||||
|
const logoRef = useTemplateRef('logoRef');
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.items,
|
||||||
|
items => {
|
||||||
|
circleRefs.value = new Array(items.length).fill(null);
|
||||||
|
tlRefs.value = new Array(items.length).fill(null);
|
||||||
|
activeTweenRefs.value = new Array(items.length).fill(null);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const layout = () => {
|
||||||
|
circleRefs.value.forEach(circle => {
|
||||||
|
if (!circle?.parentElement) return;
|
||||||
|
|
||||||
|
const pill = circle.parentElement as HTMLElement;
|
||||||
|
const rect = pill.getBoundingClientRect();
|
||||||
|
const { width: w, height: h } = rect;
|
||||||
|
const R = ((w * w) / 4 + h * h) / (2 * h);
|
||||||
|
const D = Math.ceil(2 * R) + 2;
|
||||||
|
const delta = Math.ceil(R - Math.sqrt(Math.max(0, R * R - (w * w) / 4))) + 1;
|
||||||
|
const originY = D - delta;
|
||||||
|
|
||||||
|
circle.style.width = `${D}px`;
|
||||||
|
circle.style.height = `${D}px`;
|
||||||
|
circle.style.bottom = `-${delta}px`;
|
||||||
|
|
||||||
|
gsap.set(circle, {
|
||||||
|
xPercent: -50,
|
||||||
|
scale: 0,
|
||||||
|
transformOrigin: `50% ${originY}px`
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = pill.querySelector<HTMLElement>('.pill-label');
|
||||||
|
const white = pill.querySelector<HTMLElement>('.pill-label-hover');
|
||||||
|
|
||||||
|
if (label) gsap.set(label, { y: 0 });
|
||||||
|
if (white) gsap.set(white, { y: h + 12, opacity: 0 });
|
||||||
|
|
||||||
|
const index = circleRefs.value.indexOf(circle);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
tlRefs.value[index]?.kill();
|
||||||
|
const tl = gsap.timeline({ paused: true });
|
||||||
|
|
||||||
|
tl.to(circle, { scale: 1.2, xPercent: -50, duration: 2, ease: props.ease, overwrite: 'auto' }, 0);
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
tl.to(label, { y: -(h + 8), duration: 2, ease: props.ease, overwrite: 'auto' }, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (white) {
|
||||||
|
gsap.set(white, { y: Math.ceil(h + 100), opacity: 0 });
|
||||||
|
tl.to(white, { y: 0, opacity: 1, duration: 2, ease: props.ease, overwrite: 'auto' }, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
tlRefs.value[index] = tl;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResize = () => layout();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
layout();
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
|
if (document.fonts) {
|
||||||
|
document.fonts.ready.then(layout).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = mobileMenuRef.value;
|
||||||
|
if (menu) {
|
||||||
|
gsap.set(menu, { visibility: 'hidden', opacity: 0, scaleY: 1, y: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.initialLoadAnimation) {
|
||||||
|
const logo = logoRef.value;
|
||||||
|
const navItems = navItemsRef.value;
|
||||||
|
|
||||||
|
if (logo) {
|
||||||
|
gsap.set(logo, { scale: 0 });
|
||||||
|
gsap.to(logo, {
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.6,
|
||||||
|
ease: props.ease
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navItems) {
|
||||||
|
gsap.set(navItems, { width: 0, overflow: 'hidden' });
|
||||||
|
gsap.to(navItems, {
|
||||||
|
width: 'auto',
|
||||||
|
duration: 0.6,
|
||||||
|
ease: props.ease
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.items, props.ease, props.initialLoadAnimation],
|
||||||
|
() => {
|
||||||
|
layout();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEnter = (i: number) => {
|
||||||
|
const tl = tlRefs.value[i];
|
||||||
|
if (!tl) return;
|
||||||
|
activeTweenRefs.value[i]?.kill();
|
||||||
|
activeTweenRefs.value[i] = tl.tweenTo(tl.duration(), {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: props.ease,
|
||||||
|
overwrite: 'auto'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeave = (i: number) => {
|
||||||
|
const tl = tlRefs.value[i];
|
||||||
|
if (!tl) return;
|
||||||
|
activeTweenRefs.value[i]?.kill();
|
||||||
|
activeTweenRefs.value[i] = tl.tweenTo(0, {
|
||||||
|
duration: 0.2,
|
||||||
|
ease: props.ease,
|
||||||
|
overwrite: 'auto'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoEnter = () => {
|
||||||
|
const img = logoImgRef.value;
|
||||||
|
if (!img) return;
|
||||||
|
logoTweenRef.value?.kill();
|
||||||
|
gsap.set(img, { rotate: 0 });
|
||||||
|
logoTweenRef.value = gsap.to(img, {
|
||||||
|
rotate: 360,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: props.ease,
|
||||||
|
overwrite: 'auto'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
const newState = !isMobileMenuOpen.value;
|
||||||
|
isMobileMenuOpen.value = newState;
|
||||||
|
|
||||||
|
const hamburger = hamburgerRef.value;
|
||||||
|
const menu = mobileMenuRef.value;
|
||||||
|
|
||||||
|
if (hamburger) {
|
||||||
|
const lines = hamburger.querySelectorAll('.hamburger-line');
|
||||||
|
if (newState) {
|
||||||
|
gsap.to(lines[0], { rotation: 45, y: 3, duration: 0.3, ease: props.ease });
|
||||||
|
gsap.to(lines[1], { rotation: -45, y: -3, duration: 0.3, ease: props.ease });
|
||||||
|
} else {
|
||||||
|
gsap.to(lines[0], { rotation: 0, y: 0, duration: 0.3, ease: props.ease });
|
||||||
|
gsap.to(lines[1], { rotation: 0, y: 0, duration: 0.3, ease: props.ease });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu) {
|
||||||
|
if (newState) {
|
||||||
|
gsap.set(menu, { visibility: 'visible' });
|
||||||
|
gsap.fromTo(
|
||||||
|
menu,
|
||||||
|
{ opacity: 0, y: 10, scaleY: 1 },
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scaleY: 1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: props.ease,
|
||||||
|
transformOrigin: 'top center'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
gsap.to(menu, {
|
||||||
|
opacity: 0,
|
||||||
|
y: 10,
|
||||||
|
scaleY: 1,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: props.ease,
|
||||||
|
transformOrigin: 'top center',
|
||||||
|
onComplete: () => {
|
||||||
|
gsap.set(menu, { visibility: 'hidden' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onMobileMenuClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExternalLink = (href: string) =>
|
||||||
|
href.startsWith('http://') ||
|
||||||
|
href.startsWith('https://') ||
|
||||||
|
href.startsWith('//') ||
|
||||||
|
href.startsWith('mailto:') ||
|
||||||
|
href.startsWith('tel:') ||
|
||||||
|
href.startsWith('#');
|
||||||
|
|
||||||
|
const isRouterLink = (href?: string) => href && !isExternalLink(href);
|
||||||
|
|
||||||
|
const cssVars = computed(() => ({
|
||||||
|
'--base': props.baseColor,
|
||||||
|
'--pill-bg': props.pillColor,
|
||||||
|
'--hover-text': props.hoveredPillTextColor,
|
||||||
|
'--pill-text': resolvedPillTextColor,
|
||||||
|
'--nav-h': '42px',
|
||||||
|
'--logo': '36px',
|
||||||
|
'--pill-pad-x': '18px',
|
||||||
|
'--pill-gap': '3px'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setCircleRef = (el: HTMLSpanElement | null, index: number) => {
|
||||||
|
if (circleRefs.value.length > index) {
|
||||||
|
circleRefs.value[index] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="top-[1em] left-0 md:left-auto z-[1000] absolute w-full md:w-auto">
|
||||||
|
<nav
|
||||||
|
:class="['w-full md:w-max flex items-center justify-between md:justify-start box-border px-4 md:px-0', className]"
|
||||||
|
aria-label="Primary"
|
||||||
|
:style="cssVars"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="isRouterLink(items?.[0]?.href) ? 'RouterLink' : 'a'"
|
||||||
|
:to="isRouterLink(items?.[0]?.href) ? items[0].href : undefined"
|
||||||
|
:href="!isRouterLink(items?.[0]?.href) ? items?.[0]?.href || '#' : undefined"
|
||||||
|
aria-label="Home"
|
||||||
|
role="menuitem"
|
||||||
|
ref="logoRef"
|
||||||
|
class="inline-flex justify-center items-center p-2 rounded-full overflow-hidden"
|
||||||
|
:style="{
|
||||||
|
width: 'var(--nav-h)',
|
||||||
|
height: 'var(--nav-h)',
|
||||||
|
background: 'var(--base, #000)'
|
||||||
|
}"
|
||||||
|
@mouseenter="handleLogoEnter"
|
||||||
|
>
|
||||||
|
<img :src="logo" :alt="logoAlt" ref="logoImgRef" class="block w-full h-full object-cover" />
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="navItemsRef"
|
||||||
|
class="hidden relative md:flex items-center ml-2 rounded-full"
|
||||||
|
:style="{
|
||||||
|
height: 'var(--nav-h)',
|
||||||
|
background: 'var(--base, #000)'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ul role="menubar" class="flex items-stretch m-0 p-[3px] h-full list-none" :style="{ gap: 'var(--pill-gap)' }">
|
||||||
|
<li v-for="(item, i) in items" :key="item.href || `item-${i}`" class="flex h-full" role="none">
|
||||||
|
<component
|
||||||
|
:is="isRouterLink(item.href) ? 'RouterLink' : 'a'"
|
||||||
|
:to="isRouterLink(item.href) ? item.href : undefined"
|
||||||
|
:href="!isRouterLink(item.href) ? item.href : undefined"
|
||||||
|
class="inline-flex box-border relative justify-center items-center px-0 rounded-full h-full overflow-hidden font-semibold text-[16px] no-underline uppercase leading-[0] tracking-[0.2px] whitespace-nowrap cursor-pointer"
|
||||||
|
:style="{
|
||||||
|
background: 'var(--pill-bg, #fff)',
|
||||||
|
color: 'var(--pill-text, var(--base, #000))',
|
||||||
|
paddingLeft: 'var(--pill-pad-x)',
|
||||||
|
paddingRight: 'var(--pill-pad-x)'
|
||||||
|
}"
|
||||||
|
:aria-label="item.ariaLabel || item.label"
|
||||||
|
@mouseenter="handleEnter(i)"
|
||||||
|
@mouseleave="handleLeave(i)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block bottom-0 left-1/2 z-[1] absolute rounded-full pointer-events-none hover-circle"
|
||||||
|
:style="{
|
||||||
|
background: 'var(--base, #000)',
|
||||||
|
willChange: 'transform'
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
:ref="el => setCircleRef(el as HTMLSpanElement, i)"
|
||||||
|
/>
|
||||||
|
<span class="inline-block z-[2] relative leading-[1] label-stack">
|
||||||
|
<span class="inline-block z-[2] relative leading-[1] pill-label" :style="{ willChange: 'transform' }">
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-block top-0 left-0 z-[3] absolute pill-label-hover"
|
||||||
|
:style="{
|
||||||
|
color: 'var(--hover-text, #fff)',
|
||||||
|
willChange: 'transform, opacity'
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="activeHref === item.href"
|
||||||
|
class="-bottom-[6px] left-1/2 z-[4] absolute rounded-full w-3 h-3 -translate-x-1/2"
|
||||||
|
:style="{ background: 'var(--base, #000)' }"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</component>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
ref="hamburgerRef"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
:aria-expanded="isMobileMenuOpen"
|
||||||
|
class="md:hidden relative flex flex-col justify-center items-center gap-1 p-0 border-0 rounded-full cursor-pointer"
|
||||||
|
:style="{
|
||||||
|
width: 'var(--nav-h)',
|
||||||
|
height: 'var(--nav-h)',
|
||||||
|
background: 'var(--base, #000)'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="rounded w-4 h-0.5 origin-center transition-all duration-[10ms] ease-[cubic-bezier(0.25,0.1,0.25,1)] hamburger-line"
|
||||||
|
:style="{ background: 'var(--pill-bg, #fff)' }"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="rounded w-4 h-0.5 origin-center transition-all duration-[10ms] ease-[cubic-bezier(0.25,0.1,0.25,1)] hamburger-line"
|
||||||
|
:style="{ background: 'var(--pill-bg, #fff)' }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="mobileMenuRef"
|
||||||
|
class="md:hidden top-[3em] right-4 left-4 z-[998] absolute shadow-[0_8px_32px_rgba(0,0,0,0.12)] rounded-[27px] origin-top"
|
||||||
|
:style="{
|
||||||
|
...cssVars,
|
||||||
|
background: 'var(--base, #f0f0f0)'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ul class="flex flex-col gap-[3px] m-0 p-[3px] list-none">
|
||||||
|
<li v-for="item in items" :key="item.href || `mobile-${item.label}`">
|
||||||
|
<component
|
||||||
|
:is="isRouterLink(item.href) ? 'RouterLink' : 'a'"
|
||||||
|
:to="isRouterLink(item.href) ? item.href : undefined"
|
||||||
|
:href="!isRouterLink(item.href) ? item.href : undefined"
|
||||||
|
class="block px-4 py-3 rounded-[50px] font-medium text-[16px] transition-all duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]"
|
||||||
|
:style="{ background: 'var(--pill-bg, #fff)', color: 'var(--pill-text, #fff)' }"
|
||||||
|
@mouseenter="
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.background = 'var(--base)';
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.color = 'var(--hover-text, #fff)';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@mouseleave="
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.background = 'var(--pill-bg, #fff)';
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.color = 'var(--pill-text, #fff)';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</component>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -106,6 +106,31 @@
|
|||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demo-container-dots {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-dots::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-image: radial-gradient(circle, #165f2b 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
mask:
|
||||||
|
linear-gradient(to right, transparent 0%, black 35%, black 55%, transparent 100%),
|
||||||
|
linear-gradient(to bottom, transparent 0%, black 35%, black 55%, transparent 100%);
|
||||||
|
mask-composite: intersect;
|
||||||
|
-webkit-mask:
|
||||||
|
linear-gradient(to right, transparent 0%, black 35%, black 55%, transparent 100%),
|
||||||
|
linear-gradient(to bottom, transparent 0%, black 35%, black 55%, transparent 100%);
|
||||||
|
-webkit-mask-composite: source-in;
|
||||||
|
}
|
||||||
|
|
||||||
div:has(> .props-table) {
|
div:has(> .props-table) {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|||||||
193
src/demo/Components/PillNavDemo.vue
Normal file
193
src/demo/Components/PillNavDemo.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div
|
||||||
|
class="relative overflow-hidden demo-container demo-container-dots"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: currentTheme.backgroundColor,
|
||||||
|
minHeight: '300px',
|
||||||
|
height: 'auto'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<PillNav
|
||||||
|
:key="key"
|
||||||
|
:logo="currentTheme.logo"
|
||||||
|
:baseColor="currentTheme.baseColor"
|
||||||
|
:pillColor="currentTheme.pillColor"
|
||||||
|
:hoveredPillTextColor="currentTheme.hoveredPillTextColor"
|
||||||
|
:pillTextColor="currentTheme.pillTextColor"
|
||||||
|
:initialLoadAnimation="initialLoadAnimation"
|
||||||
|
:items="[{ label: 'Home' }, { label: 'About' }, { label: 'Contact' }]"
|
||||||
|
activeHref="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSelect title="Example" :options="themeOptions" v-model="theme" />
|
||||||
|
<PreviewSwitch title="Initial Load Animation" v-model="initialLoadAnimation" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['gsap']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="pillNav" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="pillNav.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import logoDark from '../../assets/logos/vue-bits-logo-small-dark.svg';
|
||||||
|
import logoLight from '../../assets/logos/vue-bits-logo-small.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 PreviewSelect from '../../components/common/PreviewSelect.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import { pillNav } from '../../constants/code/Components/PillNavCode';
|
||||||
|
import PillNav from '../../content/Components/PillNav/PillNav.vue';
|
||||||
|
|
||||||
|
const { rerenderKey: key, forceRerender } = useForceRerender();
|
||||||
|
|
||||||
|
type ThemeKey = 'light' | 'dark' | 'color';
|
||||||
|
|
||||||
|
interface ThemeConfig {
|
||||||
|
logo: string;
|
||||||
|
baseColor: string;
|
||||||
|
pillColor: string;
|
||||||
|
hoveredPillTextColor: string;
|
||||||
|
pillTextColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = ref<ThemeKey>('light');
|
||||||
|
const initialLoadAnimation = ref(false);
|
||||||
|
|
||||||
|
const themeConfigs: Record<ThemeKey, ThemeConfig> = {
|
||||||
|
light: {
|
||||||
|
logo: logoLight,
|
||||||
|
baseColor: '#000',
|
||||||
|
pillColor: '#f0f0f0',
|
||||||
|
hoveredPillTextColor: '#fff',
|
||||||
|
pillTextColor: '#000',
|
||||||
|
backgroundColor: '#f0f0f0'
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
logo: logoDark,
|
||||||
|
baseColor: '#fff',
|
||||||
|
pillColor: '#060010',
|
||||||
|
hoveredPillTextColor: '#000',
|
||||||
|
pillTextColor: '#fff',
|
||||||
|
backgroundColor: '#060010'
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
logo: logoDark,
|
||||||
|
baseColor: '#27FF64',
|
||||||
|
pillColor: '#060010',
|
||||||
|
hoveredPillTextColor: '#060010',
|
||||||
|
pillTextColor: '#fff',
|
||||||
|
backgroundColor: '#060010'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeOptions = [
|
||||||
|
{ value: 'light', label: 'Light' },
|
||||||
|
{ value: 'dark', label: 'Dark' },
|
||||||
|
{ value: 'color', label: 'Color' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentTheme = computed(() => themeConfigs[theme.value]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[currentTheme, initialLoadAnimation],
|
||||||
|
() => {
|
||||||
|
forceRerender();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'logo',
|
||||||
|
type: 'string',
|
||||||
|
default: '-',
|
||||||
|
description: 'URL for the logo image'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'logoAlt',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Logo',
|
||||||
|
description: 'Alt text for the logo image'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'PillNavItem[]',
|
||||||
|
default: '-',
|
||||||
|
description: 'Array of navigation items with label, href, and optional ariaLabel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'activeHref',
|
||||||
|
type: 'string',
|
||||||
|
default: 'undefined',
|
||||||
|
description: 'The href of the currently active navigation item'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: "''",
|
||||||
|
description: 'Additional CSS classes for the navigation container'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ease',
|
||||||
|
type: 'string',
|
||||||
|
default: 'power3.easeOut',
|
||||||
|
description: 'GSAP easing function for animations'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'baseColor',
|
||||||
|
type: 'string',
|
||||||
|
default: '#fff',
|
||||||
|
description: 'Base background color for the navigation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pillColor',
|
||||||
|
type: 'string',
|
||||||
|
default: '#060010',
|
||||||
|
description: 'Background color for navigation pills'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hoveredPillTextColor',
|
||||||
|
type: 'string',
|
||||||
|
default: '#060010',
|
||||||
|
description: 'Text color when hovering over pills'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pillTextColor',
|
||||||
|
type: 'string',
|
||||||
|
default: 'baseColor',
|
||||||
|
description: 'Text color for navigation pills'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onMobileMenuClick',
|
||||||
|
type: '() => void',
|
||||||
|
default: 'undefined',
|
||||||
|
description: 'Callback function triggered when mobile menu button is clicked'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'initialLoadAnimation',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Enable initial load animation for logo scale and nav items reveal'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user