mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
8.2 KiB
JSON
1 line
8.2 KiB
JSON
{"name":"CardNav","title":"CardNav","description":"Expandable navigation bar with card panels revealing nested links.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { gsap } from 'gsap';\nimport { nextTick, onBeforeUpdate, onMounted, onUnmounted, ref, watch, type VNodeRef } from 'vue';\n\ntype CardNavLink = {\n label: string;\n href?: string;\n ariaLabel: string;\n};\n\nexport type CardNavItem = {\n label: string;\n bgColor: string;\n textColor: string;\n links: CardNavLink[];\n};\n\nexport interface CardNavProps {\n logo: string;\n logoAlt?: string;\n items: CardNavItem[];\n className?: string;\n ease?: string;\n baseColor?: string;\n menuColor?: string;\n buttonBgColor?: string;\n buttonTextColor?: string;\n}\n\nconst props = withDefaults(defineProps<CardNavProps>(), {\n logoAlt: 'Logo',\n className: '',\n ease: 'power3.out',\n baseColor: '#fff'\n});\n\nconst isHamburgerOpen = ref(false);\nconst isExpanded = ref(false);\n\nconst navRef = ref<HTMLDivElement | null>(null);\nconst cardsRef = ref<HTMLDivElement[]>([]);\nconst tlRef = ref<gsap.core.Timeline | null>(null);\n\nconst setCardRef =\n (i: number): VNodeRef =>\n el => {\n if (el && el instanceof HTMLDivElement) {\n cardsRef.value[i] = el;\n }\n };\n\nonBeforeUpdate(() => {\n cardsRef.value = [];\n});\n\nconst calculateHeight = () => {\n const navEl = navRef.value;\n if (!navEl) return 260;\n\n const isMobile = window.matchMedia('(max-width: 768px)').matches;\n if (isMobile) {\n const contentEl = navEl.querySelector('.card-nav-content') as HTMLElement;\n if (contentEl) {\n const wasVisible = contentEl.style.visibility;\n const wasPosition = contentEl.style.position;\n const wasHeight = contentEl.style.height;\n\n contentEl.style.visibility = 'visible';\n contentEl.style.position = 'static';\n contentEl.style.height = 'auto';\n\n const topBar = 60;\n const padding = 16;\n const contentHeight = contentEl.scrollHeight;\n\n contentEl.style.visibility = wasVisible;\n contentEl.style.position = wasPosition;\n contentEl.style.height = wasHeight;\n\n return topBar + contentHeight + padding;\n }\n }\n return 260;\n};\n\nconst createTimeline = () => {\n const navEl = navRef.value;\n if (!navEl) return null;\n\n gsap.set(navEl, { height: 60, overflow: 'hidden' });\n gsap.set(cardsRef.value, { y: 50, opacity: 0 });\n\n const tl = gsap.timeline({ paused: true });\n\n tl.to(navEl, {\n height: calculateHeight,\n duration: 0.4,\n ease: props.ease\n });\n\n tl.to(cardsRef.value, { y: 0, opacity: 1, duration: 0.4, ease: props.ease, stagger: 0.08 }, '-=0.1');\n\n return tl;\n};\n\nconst toggleMenu = () => {\n const tl = tlRef.value;\n if (!tl) return;\n if (!isExpanded.value) {\n isHamburgerOpen.value = true;\n isExpanded.value = true;\n nextTick(() => {\n tl.play(0);\n });\n } else {\n isHamburgerOpen.value = false;\n tl.eventCallback('onReverseComplete', () => {\n isExpanded.value = false;\n tl.eventCallback('onReverseComplete', null);\n });\n tl.reverse();\n }\n};\n\nconst handleResize = () => {\n if (!tlRef.value) return;\n\n if (isExpanded.value) {\n const newHeight = calculateHeight();\n gsap.set(navRef.value, { height: newHeight });\n\n tlRef.value.kill();\n const newTl = createTimeline();\n if (newTl) {\n newTl.progress(1);\n tlRef.value = newTl;\n }\n } else {\n tlRef.value.kill();\n tlRef.value = createTimeline();\n }\n};\n\nonMounted(() => {\n tlRef.value = createTimeline();\n window.addEventListener('resize', handleResize);\n});\n\nonUnmounted(() => {\n tlRef.value?.kill();\n tlRef.value = null;\n window.removeEventListener('resize', handleResize);\n});\n\nwatch(\n () => [props.ease, props.items],\n () => {\n nextTick(() => {\n if (tlRef.value) tlRef.value.kill();\n tlRef.value = createTimeline();\n });\n }\n);\n</script>\n\n<template>\n <div\n :class=\"`card-nav-container absolute left-1/2 -translate-x-1/2 w-[90%] max-w-[800px] z-[99] top-[1.2em] md:top-[2em] ${props.className}`\"\n >\n <nav\n ref=\"navRef\"\n :class=\"[\n 'card-nav block h-[60px] p-0 rounded-xl shadow-md relative overflow-hidden will-change-[height]',\n { open: isExpanded }\n ]\"\n :style=\"{ backgroundColor: props.baseColor }\"\n >\n <div\n class=\"card-nav-top top-0 z-[2] absolute inset-x-0 flex justify-between items-center p-2 px-[1.1rem] h-[60px]\"\n >\n <div\n :class=\"[\n 'hamburger-menu group h-full flex flex-col items-center justify-center cursor-pointer gap-[6px] order-2 md:order-none',\n { open: isHamburgerOpen }\n ]\"\n @click=\"toggleMenu\"\n role=\"button\"\n :aria-label=\"isExpanded ? 'Close menu' : 'Open menu'\"\n tabindex=\"0\"\n :style=\"{ color: props.menuColor || '#000' }\"\n >\n <div\n :class=\"[\n 'hamburger-line w-[30px] h-[2px] bg-current transition-[transform,opacity,margin] duration-300 ease-linear [transform-origin:50%_50%] group-hover:opacity-75',\n { 'translate-y-[4px] rotate-45': isHamburgerOpen }\n ]\"\n />\n <div\n :class=\"[\n 'hamburger-line w-[30px] h-[2px] bg-current transition-[transform,opacity,margin] duration-300 ease-linear [transform-origin:50%_50%] group-hover:opacity-75',\n { '-translate-y-[4px] -rotate-45': isHamburgerOpen }\n ]\"\n />\n </div>\n\n <div\n class=\"md:top-1/2 md:left-1/2 md:absolute flex items-center order-1 md:order-none md:-translate-x-1/2 md:-translate-y-1/2 logo-container\"\n >\n <img :src=\"props.logo\" :alt=\"props.logoAlt\" class=\"h-[28px] logo\" />\n </div>\n\n <button\n type=\"button\"\n class=\"hidden md:inline-flex px-4 py-2 border-0 rounded-[calc(0.75rem-0.2rem)] h-full font-medium transition-colors duration-300 cursor-pointer card-nav-cta-button\"\n :style=\"{\n backgroundColor: props.buttonBgColor,\n color: props.buttonTextColor\n }\"\n >\n Get Started\n </button>\n </div>\n\n <div\n :class=\"[\n 'card-nav-content absolute left-0 right-0 top-[60px] bottom-0 p-2 flex flex-col items-stretch gap-2 justify-start z-[1] md:flex-row md:items-end md:gap-[12px]',\n isExpanded ? 'visible pointer-events-auto' : 'invisible pointer-events-none'\n ]\"\n :aria-hidden=\"!isExpanded\"\n >\n <div\n v-for=\"(item, idx) in (props.items || []).slice(0, 3)\"\n :key=\"`${item.label}-${idx}`\"\n :ref=\"setCardRef(idx)\"\n class=\"relative flex flex-col flex-[1_1_auto] md:flex-[1_1_0%] gap-2 p-[12px_16px] rounded-[calc(0.75rem-0.2rem)] min-w-0 h-auto md:h-full min-h-[60px] md:min-h-0 select-none nav-card\"\n :style=\"{ backgroundColor: item.bgColor, color: item.textColor }\"\n >\n <div class=\"font-normal text-[18px] md:text-[22px] tracking-[-0.5px] nav-card-label\">\n {{ item.label }}\n </div>\n <div class=\"flex flex-col gap-[2px] mt-auto nav-card-links\">\n <a\n v-for=\"(lnk, i) in item.links\"\n :key=\"`${lnk.label}-${i}`\"\n class=\"inline-flex items-center gap-[6px] hover:opacity-75 text-[15px] md:text-[16px] no-underline transition-opacity duration-300 cursor-pointer nav-card-link\"\n :href=\"lnk.href\"\n :aria-label=\"lnk.ariaLabel\"\n >\n <v-icon name=\"go-arrow-up-right\" class=\"nav-card-link-icon shrink-0\" aria-hidden=\"true\" />\n {{ lnk.label }}\n </a>\n </div>\n </div>\n </div>\n </nav>\n </div>\n</template>\n","path":"CardNav/CardNav.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Components"]} |