adding layout components
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
// [{ route: '/path', label: 'Label' }]
|
||||||
|
routes: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="flex flex-wrap items-center gap-1.5 text-sm">
|
||||||
|
<li
|
||||||
|
v-for="({ route, label }, index) in routes"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<!-- Separator (skip for first item) -->
|
||||||
|
<span
|
||||||
|
v-if="index > 0"
|
||||||
|
class="material-icons text-base leading-none text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
chevron_right
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Link (all except last) -->
|
||||||
|
<a
|
||||||
|
v-if="index < routes.length - 1"
|
||||||
|
:href="route"
|
||||||
|
class="font-medium text-secondary no-underline transition-colors hover:text-dark"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Current page (last item) -->
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="font-medium text-dark"
|
||||||
|
aria-current="page"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
brand: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ name: 'Brand', logo: '', route: '/' }),
|
||||||
|
},
|
||||||
|
// [{ icon: '<i class="fab fa-...">' or material-icon-name, link, label? }]
|
||||||
|
socials: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// [{ name: 'Column Title', items: [{ name, href }] }]
|
||||||
|
menus: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
copyright: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const year = new Date().getFullYear()
|
||||||
|
|
||||||
|
// Detect HTML-string icons vs material icon names
|
||||||
|
function isHtml(str) {
|
||||||
|
return typeof str === 'string' && str.trimStart().startsWith('<')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="border-t border-gray-100 bg-white pt-12 pb-8">
|
||||||
|
<div class="mx-auto max-w-7xl px-6">
|
||||||
|
<div class="grid gap-8 lg:grid-cols-5">
|
||||||
|
|
||||||
|
<!-- Brand column -->
|
||||||
|
<div class="lg:col-span-1">
|
||||||
|
<a :href="brand.route" class="inline-block">
|
||||||
|
<img v-if="brand.logo" :src="brand.logo" :alt="brand.name" class="mb-3 h-8 w-auto" />
|
||||||
|
<span v-else class="text-base font-bold text-dark">{{ brand.name }}</span>
|
||||||
|
</a>
|
||||||
|
<p v-if="!brand.logo" class="mt-2 text-xs text-secondary">{{ brand.name }}</p>
|
||||||
|
|
||||||
|
<!-- Socials -->
|
||||||
|
<div v-if="socials.length" class="mt-4 flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
v-for="s in socials"
|
||||||
|
:key="s.link"
|
||||||
|
:href="s.link"
|
||||||
|
:aria-label="s.label"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="flex h-8 w-8 items-center justify-center rounded-lg text-secondary transition-colors hover:bg-gray-100 hover:text-dark"
|
||||||
|
>
|
||||||
|
<!-- HTML icon string (Font Awesome etc.) -->
|
||||||
|
<span v-if="isHtml(s.icon)" v-html="s.icon" />
|
||||||
|
<!-- Material icon name -->
|
||||||
|
<span v-else class="material-icons text-base leading-none">{{ s.icon }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu columns -->
|
||||||
|
<div
|
||||||
|
v-for="menu in menus"
|
||||||
|
:key="menu.name"
|
||||||
|
class="lg:col-span-1"
|
||||||
|
>
|
||||||
|
<h6 class="mb-3 text-xs font-bold uppercase tracking-wider text-dark">
|
||||||
|
{{ menu.name }}
|
||||||
|
</h6>
|
||||||
|
<ul class="space-y-2 list-none p-0">
|
||||||
|
<li v-for="item in menu.items" :key="item.name">
|
||||||
|
<a
|
||||||
|
:href="item.href"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="text-sm text-secondary no-underline transition-colors hover:text-dark"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Copyright -->
|
||||||
|
<div class="mt-10 border-t border-gray-100 pt-6 text-center">
|
||||||
|
<p class="text-xs text-secondary">
|
||||||
|
{{ copyright || `Copyright © ${year} — All rights reserved.` }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
// [{ name, href }]
|
||||||
|
links: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// [{ icon: HTML-string or material-icon-name, link, label? }]
|
||||||
|
socials: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
copyright: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const year = new Date().getFullYear()
|
||||||
|
|
||||||
|
function isHtml(str) {
|
||||||
|
return typeof str === 'string' && str.trimStart().startsWith('<')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="border-t border-gray-100 bg-white py-10">
|
||||||
|
<div class="mx-auto max-w-7xl px-6">
|
||||||
|
|
||||||
|
<!-- Nav links -->
|
||||||
|
<div v-if="links.length" class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
|
||||||
|
<a
|
||||||
|
v-for="link in links"
|
||||||
|
:key="link.name"
|
||||||
|
:href="link.href"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="text-sm text-secondary no-underline transition-colors hover:text-dark"
|
||||||
|
>
|
||||||
|
{{ link.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social icons -->
|
||||||
|
<div v-if="socials.length" class="mt-6 flex items-center justify-center gap-4">
|
||||||
|
<a
|
||||||
|
v-for="s in socials"
|
||||||
|
:key="s.link"
|
||||||
|
:href="s.link"
|
||||||
|
:aria-label="s.label"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-secondary transition-colors hover:bg-gray-100 hover:text-dark"
|
||||||
|
>
|
||||||
|
<span v-if="isHtml(s.icon)" v-html="s.icon" />
|
||||||
|
<span v-else class="material-icons text-lg leading-none">{{ s.icon }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Copyright -->
|
||||||
|
<p class="mt-6 text-center text-xs text-secondary">
|
||||||
|
{{ copyright || `Copyright © ${year} — All rights reserved.` }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
image: { type: String, default: '' },
|
||||||
|
// String, or { text, variant: 'h1'–'h6', class }
|
||||||
|
title: { type: [String, Object], default: '' },
|
||||||
|
// String, or { text, class }
|
||||||
|
description: { type: [String, Object], default: '' },
|
||||||
|
// Overlay color name, or full class string, or '' for no overlay
|
||||||
|
mask: { type: String, default: 'dark' },
|
||||||
|
// 0.0–1.0 overlay opacity
|
||||||
|
maskOpacity: { type: Number, default: 0.5 },
|
||||||
|
center: { type: Boolean, default: false },
|
||||||
|
minHeight: { type: String, default: '75vh' },
|
||||||
|
fullWidth: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
const titleText = computed(() => typeof props.title === 'string' ? props.title : props.title?.text ?? '')
|
||||||
|
const titleTag = computed(() => typeof props.title === 'object' && props.title?.variant ? props.title.variant : 'h1')
|
||||||
|
const titleClass = computed(() => typeof props.title === 'object' ? props.title?.class ?? '' : '')
|
||||||
|
|
||||||
|
const descText = computed(() => typeof props.description === 'string' ? props.description : props.description?.text ?? '')
|
||||||
|
const descClass = computed(() => typeof props.description === 'object' ? props.description?.class ?? '' : '')
|
||||||
|
|
||||||
|
// Resolve mask to a gradient background class or custom class
|
||||||
|
const gradientMap = {
|
||||||
|
primary: 'bg-gradient-primary',
|
||||||
|
secondary: 'bg-gradient-secondary',
|
||||||
|
success: 'bg-gradient-success',
|
||||||
|
warning: 'bg-gradient-warning',
|
||||||
|
danger: 'bg-gradient-danger',
|
||||||
|
info: 'bg-gradient-info',
|
||||||
|
dark: 'bg-gradient-dark',
|
||||||
|
light: 'bg-gradient-light',
|
||||||
|
}
|
||||||
|
const maskClass = computed(() => gradientMap[props.mask] ?? props.mask ?? 'bg-gradient-dark')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<div
|
||||||
|
class="relative flex items-center overflow-hidden bg-cover bg-center"
|
||||||
|
:style="{ backgroundImage: image ? `url(${image})` : undefined, minHeight }"
|
||||||
|
>
|
||||||
|
<!-- Gradient mask overlay -->
|
||||||
|
<div
|
||||||
|
v-if="mask"
|
||||||
|
class="absolute inset-0"
|
||||||
|
:class="maskClass"
|
||||||
|
:style="{ opacity: maskOpacity }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div
|
||||||
|
class="relative z-10 w-full"
|
||||||
|
:class="fullWidth ? 'px-6' : 'mx-auto max-w-7xl px-6'"
|
||||||
|
>
|
||||||
|
<div :class="center ? 'mx-auto max-w-2xl text-center' : 'max-w-xl'">
|
||||||
|
<component
|
||||||
|
:is="titleTag"
|
||||||
|
class="font-bold text-white"
|
||||||
|
:class="[titleClass, center ? 'text-4xl lg:text-5xl' : 'text-3xl lg:text-4xl']"
|
||||||
|
>
|
||||||
|
{{ titleText }}
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="descText"
|
||||||
|
class="mt-4 text-lg text-white/80 leading-relaxed"
|
||||||
|
:class="descClass"
|
||||||
|
>
|
||||||
|
{{ descText }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
brand: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ name: 'Brand', route: '/' }),
|
||||||
|
},
|
||||||
|
// [{ label, href?, icon?, children?: [{ label, href, description? }] }]
|
||||||
|
navItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// { label, href, color }
|
||||||
|
action: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ label: 'Get Started', href: '#', color: 'primary' }),
|
||||||
|
},
|
||||||
|
transparent: { type: Boolean, default: false },
|
||||||
|
dark: { type: Boolean, default: false },
|
||||||
|
sticky: { type: Boolean, default: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Scroll blur ───────────────────────────────────────────────
|
||||||
|
const scrolled = ref(false)
|
||||||
|
|
||||||
|
function onScroll() { scrolled.value = window.scrollY > 20 }
|
||||||
|
onMounted(() => window.addEventListener('scroll', onScroll, { passive: true }))
|
||||||
|
onUnmounted(() => window.removeEventListener('scroll', onScroll))
|
||||||
|
|
||||||
|
// ── Mobile menu ───────────────────────────────────────────────
|
||||||
|
const mobileOpen = ref(false)
|
||||||
|
const openDropdown = ref(null)
|
||||||
|
|
||||||
|
function toggleDropdown(label) {
|
||||||
|
openDropdown.value = openDropdown.value === label ? null : label
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdowns on outside click
|
||||||
|
function onDocClick(e) {
|
||||||
|
if (!e.target.closest('[data-nav-item]')) openDropdown.value = null
|
||||||
|
}
|
||||||
|
onMounted(() => document.addEventListener('click', onDocClick))
|
||||||
|
onUnmounted(() => document.removeEventListener('click', onDocClick))
|
||||||
|
|
||||||
|
// ── Computed styles ───────────────────────────────────────────
|
||||||
|
const isBlurred = computed(() => props.transparent && scrolled.value)
|
||||||
|
|
||||||
|
const navClass = computed(() => {
|
||||||
|
if (props.dark) return 'bg-gradient-dark shadow-dark'
|
||||||
|
if (props.transparent && !scrolled.value) return 'bg-transparent'
|
||||||
|
return 'bg-white/90 backdrop-blur-sm shadow-soft-sm'
|
||||||
|
})
|
||||||
|
|
||||||
|
const textClass = computed(() => {
|
||||||
|
if (props.dark) return 'text-white'
|
||||||
|
if (props.transparent && !scrolled.value) return 'text-white'
|
||||||
|
return 'text-dark'
|
||||||
|
})
|
||||||
|
|
||||||
|
const actionColors = {
|
||||||
|
primary: 'bg-gradient-primary shadow-primary text-white',
|
||||||
|
secondary: 'bg-gradient-secondary shadow-secondary text-white',
|
||||||
|
success: 'bg-gradient-success shadow-success text-white',
|
||||||
|
warning: 'bg-gradient-warning shadow-warning text-white',
|
||||||
|
danger: 'bg-gradient-danger shadow-danger text-white',
|
||||||
|
info: 'bg-gradient-info shadow-info text-white',
|
||||||
|
dark: 'bg-gradient-dark shadow-dark text-white',
|
||||||
|
white: 'bg-white text-dark shadow-soft-sm',
|
||||||
|
}
|
||||||
|
const actionClass = computed(() => actionColors[props.action.color] ?? actionColors.primary)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
class="top-0 z-30 w-full transition-all duration-300"
|
||||||
|
:class="[navClass, sticky ? 'sticky' : 'absolute']"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-7xl px-4">
|
||||||
|
<div class="flex h-16 items-center justify-between">
|
||||||
|
|
||||||
|
<!-- Brand -->
|
||||||
|
<a
|
||||||
|
:href="brand.route"
|
||||||
|
class="text-sm font-bold no-underline transition-opacity hover:opacity-80"
|
||||||
|
:class="textClass"
|
||||||
|
>
|
||||||
|
{{ brand.name }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Desktop nav -->
|
||||||
|
<ul class="hidden items-center gap-1 list-none lg:flex">
|
||||||
|
<li
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="relative"
|
||||||
|
data-nav-item
|
||||||
|
>
|
||||||
|
<!-- Item with children (dropdown) -->
|
||||||
|
<template v-if="item.children?.length">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
||||||
|
:class="[textClass, 'hover:bg-black/5']"
|
||||||
|
@click.stop="toggleDropdown(item.label)"
|
||||||
|
>
|
||||||
|
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
|
||||||
|
{{ item.label }}
|
||||||
|
<span class="material-icons text-sm leading-none opacity-60 transition-transform duration-200"
|
||||||
|
:class="openDropdown === item.label ? 'rotate-180' : ''">
|
||||||
|
expand_more
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Dropdown panel -->
|
||||||
|
<Transition name="dropdown">
|
||||||
|
<div
|
||||||
|
v-if="openDropdown === item.label"
|
||||||
|
class="absolute left-0 top-full mt-2 w-52 rounded-xl bg-white p-2 shadow-soft-lg"
|
||||||
|
>
|
||||||
|
<template v-for="child in item.children" :key="child.label">
|
||||||
|
<a
|
||||||
|
:href="child.href"
|
||||||
|
class="flex flex-col rounded-lg px-3 py-2.5 text-sm text-dark no-underline transition-colors hover:bg-gray-50"
|
||||||
|
@click="openDropdown = null"
|
||||||
|
>
|
||||||
|
<span class="font-medium">{{ child.label }}</span>
|
||||||
|
<span v-if="child.description" class="mt-0.5 text-xs text-secondary">{{ child.description }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Plain link -->
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="item.href"
|
||||||
|
class="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium no-underline transition-colors"
|
||||||
|
:class="[textClass, 'hover:bg-black/5']"
|
||||||
|
>
|
||||||
|
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
|
||||||
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- CTA + mobile toggle -->
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
:href="action.href"
|
||||||
|
class="hidden rounded-lg px-5 py-2 text-sm font-medium no-underline transition-all hover:-translate-y-0.5 lg:inline-flex"
|
||||||
|
:class="actionClass"
|
||||||
|
>
|
||||||
|
{{ action.label }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Hamburger -->
|
||||||
|
<button
|
||||||
|
class="flex h-9 w-9 items-center justify-center rounded-lg transition-colors hover:bg-black/5 lg:hidden"
|
||||||
|
:class="textClass"
|
||||||
|
@click="mobileOpen = !mobileOpen"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
<span class="material-icons">{{ mobileOpen ? 'close' : 'menu' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile menu -->
|
||||||
|
<Transition name="mobile-menu">
|
||||||
|
<div v-if="mobileOpen" class="border-t border-gray-100 bg-white lg:hidden">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 py-3 space-y-1">
|
||||||
|
<template v-for="item in navItems" :key="item.label">
|
||||||
|
<!-- Mobile dropdown -->
|
||||||
|
<template v-if="item.children?.length">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium text-dark hover:bg-gray-50"
|
||||||
|
@click="toggleDropdown(item.label)"
|
||||||
|
>
|
||||||
|
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
|
||||||
|
{{ item.label }}
|
||||||
|
<span class="material-icons ml-auto text-sm leading-none opacity-60 transition-transform duration-200"
|
||||||
|
:class="openDropdown === item.label ? 'rotate-180' : ''">expand_more</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="openDropdown === item.label" class="ml-6 space-y-1">
|
||||||
|
<a
|
||||||
|
v-for="child in item.children"
|
||||||
|
:key="child.label"
|
||||||
|
:href="child.href"
|
||||||
|
class="block rounded-lg px-3 py-2 text-sm text-secondary no-underline hover:bg-gray-50 hover:text-dark"
|
||||||
|
@click="mobileOpen = false"
|
||||||
|
>
|
||||||
|
{{ child.label }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Mobile plain link -->
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="item.href"
|
||||||
|
class="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium text-dark no-underline hover:bg-gray-50"
|
||||||
|
@click="mobileOpen = false"
|
||||||
|
>
|
||||||
|
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
|
||||||
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Mobile CTA -->
|
||||||
|
<div class="pt-2 pb-1">
|
||||||
|
<a
|
||||||
|
:href="action.href"
|
||||||
|
class="flex w-full items-center justify-center rounded-lg px-5 py-2.5 text-sm font-medium no-underline"
|
||||||
|
:class="actionClass"
|
||||||
|
@click="mobileOpen = false"
|
||||||
|
>
|
||||||
|
{{ action.label }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dropdown-enter-active,
|
||||||
|
.dropdown-leave-active {
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.dropdown-enter-from,
|
||||||
|
.dropdown-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-enter-active,
|
||||||
|
.mobile-menu-leave-active {
|
||||||
|
transition: opacity 0.2s ease, max-height 0.25s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
.mobile-menu-enter-from,
|
||||||
|
.mobile-menu-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user