feat. header 컴포넌트 반응형 제작
This commit is contained in:
@@ -50,7 +50,7 @@ const props = defineProps<Props>()
|
||||
@apply transition-transform duration-300 w-full h-full object-cover;
|
||||
}
|
||||
.card-overlay {
|
||||
@apply absolute bottom-0 left-0 right-0 pt-[14px] px-[18px] pb-[16px] flex flex-col justify-end border-t border-white/10 bg-black/40 shadow-[0_-10px_10px_0_rgba(0,0,0,0.25)] backdrop-blur-[25px] md:pt-[20px] md:px-[26px] md:pb-[26px];
|
||||
@apply absolute bottom-0 left-0 right-0 pt-[14px] px-[18px] pb-[16px] flex flex-col justify-end border-t border-white/10 bg-black/40 shadow-[0_-10px_10px_0_rgba(0,0,0,0.25)] backdrop-blur-[25px] md:pt-[20px] text-left md:px-[26px] md:pb-[26px];
|
||||
}
|
||||
.card-title {
|
||||
@apply text-[14px] leading-[20px] font-medium text-white md:text-lg md:leading-[26px];
|
||||
|
||||
@@ -1,24 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
||||
import type { GameDataValue, GameDataGnb } from '#layers/types/api/gameData'
|
||||
import { useScrollStore } from '#layers/stores/useScrollStore'
|
||||
import { useWindowScroll, onClickOutside } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type {
|
||||
GameDataValue,
|
||||
GameDataMenu,
|
||||
GameDataMenuChildren,
|
||||
} from '#layers/types/api/gameData'
|
||||
|
||||
const route = useRoute()
|
||||
const { y: windowY } = useWindowScroll()
|
||||
const scrollStore = useScrollStore()
|
||||
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
|
||||
const gameData = gameDataStore.gameData as GameDataValue
|
||||
const gnbList = gameData?.gnb?.menus as GameDataGnb['menus']
|
||||
const gnbList = (gameData?.gnb?.menus ?? []) as GameDataMenuChildren
|
||||
|
||||
const isMenuOpen = ref(false)
|
||||
const contentNavRef = ref<HTMLElement>()
|
||||
|
||||
const currentPath = computed(() => formatPathWithoutLocale(route.path))
|
||||
|
||||
const pathMatches = (base: string, current: string) => {
|
||||
if (!base || base === '/') return current === '/'
|
||||
return current === base || current.startsWith(base + '/')
|
||||
}
|
||||
|
||||
/** header overlay 높이 계산 (re-compute 최소화) */
|
||||
const scrollPositionPX = computed(() => {
|
||||
const gnbHeight = scrollStore.stoveGnbHeight
|
||||
const y = windowY.value
|
||||
if (y === 0) return `${gnbHeight}px`
|
||||
if (y >= gnbHeight) return '0px'
|
||||
return `${gnbHeight - y}px`
|
||||
})
|
||||
|
||||
/** 자식 중 활성 링크 존재 여부 */
|
||||
const hasActiveChild = (children?: GameDataMenuChildren) => {
|
||||
const cur = currentPath.value
|
||||
return formatToArray(children).some(child => {
|
||||
if (!child?.url_path || !isInternalUrl(child.url_path)) return false
|
||||
return pathMatches(formatPathWithoutLocale(child.url_path), cur)
|
||||
})
|
||||
}
|
||||
|
||||
/** 1Depth 활성화 여부 */
|
||||
const isNavItemActive = (gnbItem: GameDataMenu): boolean => {
|
||||
const cur = currentPath.value
|
||||
const base = gnbItem?.url_path
|
||||
const selfActive =
|
||||
!!base &&
|
||||
isInternalUrl(base) &&
|
||||
pathMatches(formatPathWithoutLocale(base), cur)
|
||||
return selfActive || hasActiveChild(gnbItem.children)
|
||||
}
|
||||
|
||||
const handleMenuOpen = () => (isMenuOpen.value = true)
|
||||
const handleMenuClose = () => (isMenuOpen.value = false)
|
||||
|
||||
onClickOutside(contentNavRef, () => (isMenuOpen.value = false))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="bg-theme-foreground text-theme-foreground-reversal relative z-50"
|
||||
>
|
||||
<BlocksStoveGnb />
|
||||
<div
|
||||
data-name="header-game"
|
||||
class="px-[40px] h-16 flex items-center whitespace-nowrap"
|
||||
>
|
||||
<header class="header">
|
||||
<BlocksStoveGnb class="min-h-[48px]" />
|
||||
|
||||
<div class="header-game" :class="{ 'game-fixed': isPassedStoveGnb }">
|
||||
<!-- 로고 -->
|
||||
<div data-name="header-logo" class="mr-[40px]">
|
||||
<div class="header-logo">
|
||||
<AtomsLocaleLink to="/brand">
|
||||
<img
|
||||
:src="gameData?.gnb?.bi_path"
|
||||
@@ -27,70 +78,208 @@ const gnbList = gameData?.gnb?.menus as GameDataGnb['menus']
|
||||
/>
|
||||
</AtomsLocaleLink>
|
||||
</div>
|
||||
|
||||
<!-- 메인 네비게이션 -->
|
||||
<nav data-name="header-nav" class="flex items-center space-x-[32px]">
|
||||
<template v-if="gnbList">
|
||||
<div
|
||||
v-for="(gnbItem, key) in gnbList"
|
||||
:key="key"
|
||||
class="relative group"
|
||||
<button
|
||||
class="btn-open"
|
||||
aria-controls="site-menu"
|
||||
:aria-expanded="isMenuOpen"
|
||||
@click="handleMenuOpen"
|
||||
>
|
||||
<span class="sr-only">menu open</span>
|
||||
</button>
|
||||
<div
|
||||
class="header-content"
|
||||
:class="{ 'menu-open': isMenuOpen }"
|
||||
:style="{ '--scroll-position': scrollPositionPX }"
|
||||
>
|
||||
<div id="site-menu" ref="contentNavRef" class="content-nav">
|
||||
<button
|
||||
class="btn-close"
|
||||
aria-controls="site-menu"
|
||||
:aria-expanded="isMenuOpen"
|
||||
@click="handleMenuClose"
|
||||
>
|
||||
<!-- Link 컴포넌트 사용 -->
|
||||
<BlocksHybridLink
|
||||
:to="gnbItem.url_path"
|
||||
:target="gnbItem.link_target"
|
||||
class="relative flex items-center h-[64px]"
|
||||
>
|
||||
{{ gnbItem.menu_name }}
|
||||
<AtomsIconsArrowDown v-if="gnbItem.children" class="ml-1" />
|
||||
<span
|
||||
class="absolute bottom-0 left-0 w-full h-2 border-b-2 border-transparent transition-border-color group-hover:border-theme-foreground-reversal group-active:border-theme-foreground-reversal-10"
|
||||
<span class="sr-only">menu close</span>
|
||||
</button>
|
||||
<div class="nav-logo">
|
||||
<AtomsLocaleLink to="/brand">
|
||||
<img
|
||||
:src="gameData?.gnb?.bi_path"
|
||||
:alt="gameData?.game_name"
|
||||
class="h-[30px]"
|
||||
/>
|
||||
</BlocksHybridLink>
|
||||
<div
|
||||
v-if="gnbItem.children"
|
||||
class="absolute top-full left-[-28px] min-w-[190px] pt-[4px] pointer-events-none group-hover:pointer-events-auto"
|
||||
>
|
||||
<ul
|
||||
class="bg-theme-foreground-10 rounded-[20px] shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 p-3"
|
||||
>
|
||||
<li v-for="child in gnbItem.children" :key="child.menu_name">
|
||||
<!-- Link 컴포넌트 사용 -->
|
||||
<BlocksHybridLink
|
||||
:to="child.url_path"
|
||||
:target="child.link_target"
|
||||
class="flex items-center px-4 py-[9px] rounded-[12px] transition-background hover:bg-theme-foreground-reversal-40 active:bg-theme-foreground-reversal-70"
|
||||
>
|
||||
{{ child.menu_name }}
|
||||
<AtomsIconsLinkOut
|
||||
v-if="child.link_target === '_blank'"
|
||||
class="ml-1"
|
||||
/>
|
||||
</BlocksHybridLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AtomsLocaleLink>
|
||||
</div>
|
||||
</template>
|
||||
<nav class="nav-main">
|
||||
<div v-if="gnbList" class="main-official">
|
||||
<div
|
||||
v-for="(gnbItem, key) in gnbList"
|
||||
:key="key"
|
||||
class="nav-item"
|
||||
>
|
||||
<BlocksHybridLink
|
||||
:to="gnbItem.url_path"
|
||||
:target="gnbItem.link_target"
|
||||
:class="`item-1depth ${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
||||
>
|
||||
<span>{{ gnbItem.menu_name }}</span>
|
||||
<AtomsIconsArrowDown v-if="gnbItem.children" />
|
||||
</BlocksHybridLink>
|
||||
|
||||
<!-- 구분선 -->
|
||||
<div class="w-px h-4 bg-theme-foreground-reversal-30" />
|
||||
|
||||
<!-- 이벤트 -->
|
||||
<a href="#" class="flex items-center space-x-[3px] text-gradient-pink">
|
||||
<AtomsIconsStar />
|
||||
<span>이벤트</span>
|
||||
<AtomsIconsStar />
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- 오른쪽 영역 -->
|
||||
<div data-name="header-right" class="ml-auto">
|
||||
<div class="relative group">
|
||||
<AtomsButton size="small">게임 시작</AtomsButton>
|
||||
<div v-if="gnbItem.children" class="item-2depth">
|
||||
<ul>
|
||||
<li
|
||||
v-for="child in gnbItem.children"
|
||||
:key="child.menu_name"
|
||||
>
|
||||
<BlocksHybridLink
|
||||
:to="child.url_path"
|
||||
:target="child.link_target"
|
||||
>
|
||||
<span>{{ child.menu_name }}</span>
|
||||
<AtomsIconsLinkOut
|
||||
v-if="child.link_target === '_blank'"
|
||||
/>
|
||||
</BlocksHybridLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-event">
|
||||
<a href="#" class="item-1depth text-gradient-pink">
|
||||
<AtomsIconsStar />
|
||||
<span>이벤트</span>
|
||||
<AtomsIconsStar />
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="btn-start">
|
||||
<AtomsButton size="small" class="w-full md:w-auto">
|
||||
게임 시작
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
@apply bg-theme-foreground text-theme-foreground-reversal relative z-50;
|
||||
}
|
||||
.header-game {
|
||||
@apply absolute flex w-full h-[48px] items-center whitespace-nowrap px-[64px] bg-theme-foreground md:h-16 md:px-[40px]
|
||||
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-px before:bg-theme-foreground-reversal-6;
|
||||
}
|
||||
.game-fixed {
|
||||
@apply fixed top-0 left-0;
|
||||
}
|
||||
.header-logo {
|
||||
@apply mx-auto shrink-0 md:mx-0;
|
||||
}
|
||||
|
||||
.btn-open,
|
||||
.btn-close {
|
||||
@apply absolute w-[40px] h-[40px] md:hidden bg-[red];
|
||||
}
|
||||
.btn-open {
|
||||
@apply top-[4px] left-[12px];
|
||||
}
|
||||
.btn-close {
|
||||
@apply top-[11px] left-[12px];
|
||||
}
|
||||
.btn-start {
|
||||
@apply relative mt-2 px-5 md:ml-auto md:mt-0 md:px-0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
@apply overflow-hidden absolute top-0 left-0 w-0 md:overflow-visible md:static md:w-full md:h-full;
|
||||
height: calc(100vh - var(--scroll-position));
|
||||
}
|
||||
.header-content.menu-open {
|
||||
@apply w-full;
|
||||
}
|
||||
.header-content.menu-open::before {
|
||||
@apply content-[''] absolute inset-0 w-[100vw] h-full bg-[rgba(0,0,0,0.6)] md:hidden;
|
||||
}
|
||||
.header-content.menu-open .content-nav {
|
||||
@apply relative h-full translate-x-0 transition-transform duration-300 md:translate-x-0;
|
||||
}
|
||||
|
||||
.content-nav {
|
||||
@apply relative flex flex-col w-[360px] bg-theme-foreground-10 translate-x-[-100%]
|
||||
md:flex-row md:w-full md:items-center md:h-full md:bg-transparent md:translate-x-0;
|
||||
}
|
||||
|
||||
.nav-main {
|
||||
@apply overflow-hidden flex flex-col order-1 h-full mt-2 mb-4 px-2
|
||||
md:overflow-visible md:flex-row md:order-none md:h-full md:my-0 md:mx-10 md:px-0;
|
||||
}
|
||||
.main-official {
|
||||
@apply overflow-y-auto pb-2 md:overflow-visible md:flex md:items-center md:space-x-8 md:pb-0;
|
||||
}
|
||||
|
||||
.main-event {
|
||||
@apply relative flex flex-col mb-2 md:flex-row md:mb-0 md:ml-16;
|
||||
}
|
||||
.main-event::before {
|
||||
@apply content-[''] hidden absolute top-[23px] left-[-32px] w-px h-4 bg-theme-foreground-reversal-30 md:block;
|
||||
}
|
||||
.main-event::after {
|
||||
@apply order-[-1] content-[''] h-px mb-2 mx-3 bg-theme-foreground-reversal-8
|
||||
md:mb-0 md:mx-0 md:absolute md:bottom-0 md:left-0 md:w-full md:h-[2px] md:bg-theme-foreground-reversal md:opacity-0;
|
||||
}
|
||||
.main-event:hover::after {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
@apply flex items-center justify-center shrink-0 h-[62px] md:hidden;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply relative flex flex-col text-[16px] md:items-center md:h-full;
|
||||
}
|
||||
.nav-item:last-of-type::after {
|
||||
@apply hidden;
|
||||
}
|
||||
.nav-item::after {
|
||||
@apply content-[''] h-px my-2 mx-3 bg-theme-foreground-reversal-8 md:hidden;
|
||||
}
|
||||
.nav-item:hover .item-1depth::after,
|
||||
.item-1depth.active::after {
|
||||
@apply opacity-100 md:transition-opacity;
|
||||
}
|
||||
.nav-item:hover .item-2depth {
|
||||
@apply md:opacity-100 md:visible md:pointer-events-auto md:transition-opacity md:duration-200;
|
||||
}
|
||||
|
||||
.item-1depth {
|
||||
@apply flex items-center py-[9px] px-3 gap-1 rounded-[12px] md:h-full md:py-0 md:px-0;
|
||||
}
|
||||
.item-1depth::after {
|
||||
@apply md:content-[''] md:absolute md:bottom-0 md:left-0 md:w-full md:h-[2px] md:bg-theme-foreground-reversal md:opacity-0;
|
||||
}
|
||||
.item-1depth.active {
|
||||
@apply bg-theme-foreground-reversal-8 md:bg-transparent;
|
||||
}
|
||||
|
||||
.item-2depth {
|
||||
@apply block text-[15px] md:opacity-0 md:absolute md:top-full md:left-[-28px] md:min-w-[190px] md:pt-1 md:pointer-events-none;
|
||||
}
|
||||
.item-2depth ul {
|
||||
@apply bg-theme-foreground-10 rounded-[20px] md:shadow-lg md:p-3;
|
||||
}
|
||||
.item-2depth a {
|
||||
@apply flex items-center gap-1 px-5 py-[9px] rounded-[12px] transition-colors
|
||||
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10
|
||||
md:px-4 md:py-[11px];
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.header-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -57,7 +57,7 @@ watchEffect(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<main class="main">
|
||||
<template
|
||||
v-for="(template, index) in visibleTemplates"
|
||||
:key="template.template_code ?? index"
|
||||
@@ -70,3 +70,9 @@ watchEffect(() => {
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
@apply pt-[48px] md:pt-[64px];
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user