feat(frontend): restore ref main navigation
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import MainNavigationLink from './MainNavigationLink.vue';
|
||||
import {
|
||||
buildGlobalNavigation,
|
||||
isNavigationConfigured,
|
||||
type MainNavigationLink as MainNavigationLinkItem,
|
||||
} from './mainNavigation';
|
||||
import { useMenuPopup } from './useMenuPopup';
|
||||
|
||||
const props = defineProps<{
|
||||
npcMode: number;
|
||||
voteActive: boolean;
|
||||
}>();
|
||||
|
||||
const entries = computed(() => buildGlobalNavigation(props.npcMode));
|
||||
const { setRoot, openId, close, toggle } = useMenuPopup();
|
||||
const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props.voteActive;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav :ref="setRoot" class="main-global-menu" aria-label="게임 공통 메뉴">
|
||||
<template v-for="entry in entries" :key="entry.id">
|
||||
<MainNavigationLink
|
||||
v-if="entry.kind === 'link'"
|
||||
:link="entry"
|
||||
:enabled="isNavigationConfigured(entry)"
|
||||
:active="isActive(entry)"
|
||||
/>
|
||||
<div v-else-if="entry.kind === 'group'" class="main-menu-popup">
|
||||
<button
|
||||
class="main-menu-button"
|
||||
type="button"
|
||||
:data-menu-id="entry.id"
|
||||
:aria-expanded="openId === entry.id"
|
||||
:aria-controls="`global-menu-${entry.id}`"
|
||||
@click="toggle(entry.id, $event)"
|
||||
>
|
||||
{{ entry.label }}
|
||||
<span class="menu-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul
|
||||
v-show="openId === entry.id"
|
||||
:id="`global-menu-${entry.id}`"
|
||||
class="main-menu-popup__list"
|
||||
role="menu"
|
||||
>
|
||||
<template v-for="item in entry.items" :key="item.id">
|
||||
<li v-if="item.kind === 'divider'" class="main-menu-divider" role="separator"></li>
|
||||
<li v-else role="none">
|
||||
<MainNavigationLink
|
||||
:link="item"
|
||||
:enabled="isNavigationConfigured(item)"
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else class="main-menu-split">
|
||||
<MainNavigationLink
|
||||
:link="entry.main"
|
||||
:enabled="isNavigationConfigured(entry.main)"
|
||||
:active="isActive(entry.main)"
|
||||
/>
|
||||
<button
|
||||
class="main-menu-button main-menu-split__toggle"
|
||||
type="button"
|
||||
:data-menu-id="entry.id"
|
||||
:aria-label="`${entry.main.label} 하위 메뉴`"
|
||||
:aria-expanded="openId === entry.id"
|
||||
:aria-controls="`global-menu-${entry.id}`"
|
||||
@click="toggle(entry.id, $event)"
|
||||
>
|
||||
<span class="menu-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul
|
||||
v-show="openId === entry.id"
|
||||
:id="`global-menu-${entry.id}`"
|
||||
class="main-menu-popup__list"
|
||||
role="menu"
|
||||
>
|
||||
<template v-for="item in entry.items" :key="item.id">
|
||||
<li v-if="item.kind === 'divider'" class="main-menu-divider" role="separator"></li>
|
||||
<li v-else role="none">
|
||||
<MainNavigationLink
|
||||
:link="item"
|
||||
:enabled="isNavigationConfigured(item)"
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-global-menu {
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 1.6px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.main-menu-popup,
|
||||
.main-menu-split {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.main-menu-popup > .main-menu-button,
|
||||
.main-menu-split > :deep(.main-menu-link) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-menu-split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 28px;
|
||||
}
|
||||
|
||||
.main-menu-split__toggle {
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
padding: 0;
|
||||
border-left-width: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.menu-caret {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
border-top: 4px solid currentColor;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.main-menu-popup__list {
|
||||
position: absolute;
|
||||
z-index: 120;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
min-width: max(100%, 180px);
|
||||
margin: 0;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 3px;
|
||||
padding: 4px 0;
|
||||
background: #202020;
|
||||
box-shadow: 0 8px 18px rgb(0 0 0 / 45%);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.main-menu-split .main-menu-popup__list {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.main-menu-popup__list :deep(.main-menu-link) {
|
||||
min-height: 30px;
|
||||
justify-content: flex-start;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 5px 16px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.main-menu-divider {
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
background: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 939.98px) {
|
||||
.main-global-menu {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,351 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import MainNavigationLink from './MainNavigationLink.vue';
|
||||
import {
|
||||
buildGlobalNavigation,
|
||||
isNationNavigationEnabled,
|
||||
isNavigationConfigured,
|
||||
nationNavigation,
|
||||
quickNavigation,
|
||||
type MainNavigationLink as MainNavigationLinkItem,
|
||||
type NationNavigationAccess,
|
||||
type QuickNavigationItem,
|
||||
} from './mainNavigation';
|
||||
import { useMenuPopup } from './useMenuPopup';
|
||||
|
||||
const props = defineProps<{
|
||||
access: NationNavigationAccess;
|
||||
tournamentStage: number;
|
||||
nationColor: string;
|
||||
npcMode: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
lobby: [];
|
||||
quick: [item: QuickNavigationItem];
|
||||
}>();
|
||||
|
||||
const { setRoot, openId, close, toggle } = useMenuPopup();
|
||||
const globalEntries = computed(() => buildGlobalNavigation(props.npcMode));
|
||||
const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props.tournamentStage;
|
||||
|
||||
const onQuick = (item: QuickNavigationItem) => {
|
||||
close();
|
||||
emit('quick', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
:ref="setRoot"
|
||||
class="main-mobile-bottom"
|
||||
:style="{ '--nation-menu-color': nationColor || '#000000' }"
|
||||
aria-label="모바일 빠른 메뉴"
|
||||
>
|
||||
<div class="bottom-item">
|
||||
<button
|
||||
class="bottom-trigger"
|
||||
type="button"
|
||||
data-bottom-menu="global"
|
||||
:aria-expanded="openId === 'global'"
|
||||
aria-controls="mobile-global-menu"
|
||||
@click="toggle('global', $event)"
|
||||
>
|
||||
외부 메뉴
|
||||
<span class="dropup-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul v-show="openId === 'global'" id="mobile-global-menu" class="bottom-popup" role="menu">
|
||||
<template v-for="entry in globalEntries" :key="entry.id">
|
||||
<li v-if="entry.kind === 'link'" role="none">
|
||||
<MainNavigationLink
|
||||
:link="entry"
|
||||
:enabled="isNavigationConfigured(entry)"
|
||||
compact
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</li>
|
||||
<template v-else>
|
||||
<li class="bottom-heading" role="presentation">
|
||||
{{ entry.kind === 'group' ? entry.label : entry.main.label }}
|
||||
</li>
|
||||
<template v-if="entry.kind === 'split'">
|
||||
<li role="none">
|
||||
<MainNavigationLink
|
||||
:link="entry.main"
|
||||
:enabled="isNavigationConfigured(entry.main)"
|
||||
compact
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<template v-for="item in entry.items" :key="item.id">
|
||||
<li v-if="item.kind === 'divider'" class="bottom-divider" role="separator"></li>
|
||||
<li v-else role="none">
|
||||
<MainNavigationLink
|
||||
:link="item"
|
||||
:enabled="isNavigationConfigured(item)"
|
||||
compact
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bottom-item nation-bottom-item">
|
||||
<button
|
||||
class="bottom-trigger nation-trigger"
|
||||
type="button"
|
||||
data-bottom-menu="nation"
|
||||
:aria-expanded="openId === 'nation'"
|
||||
aria-controls="mobile-nation-menu"
|
||||
@click="toggle('nation', $event)"
|
||||
>
|
||||
국가 메뉴
|
||||
<span class="dropup-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul v-show="openId === 'nation'" id="mobile-nation-menu" class="bottom-popup" role="menu">
|
||||
<template v-for="entry in nationNavigation" :key="entry.id">
|
||||
<li v-if="entry.kind === 'link'" role="none">
|
||||
<MainNavigationLink
|
||||
:link="entry"
|
||||
:enabled="isNationNavigationEnabled(entry, access)"
|
||||
:active="isActive(entry)"
|
||||
compact
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</li>
|
||||
<template v-else-if="entry.kind === 'split'">
|
||||
<li role="none">
|
||||
<MainNavigationLink
|
||||
:link="entry.main"
|
||||
:enabled="isNationNavigationEnabled(entry.main, access)"
|
||||
compact
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</li>
|
||||
<li v-for="item in entry.items" :key="item.id" role="none">
|
||||
<template v-if="item.kind === 'link'">
|
||||
<MainNavigationLink
|
||||
:link="item"
|
||||
:enabled="isNationNavigationEnabled(item, access)"
|
||||
compact
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bottom-item">
|
||||
<button
|
||||
class="bottom-trigger quick-trigger"
|
||||
type="button"
|
||||
data-bottom-menu="quick"
|
||||
:aria-expanded="openId === 'quick'"
|
||||
aria-controls="mobile-quick-menu"
|
||||
@click="toggle('quick', $event)"
|
||||
>
|
||||
빠른 이동
|
||||
<span class="dropup-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul v-show="openId === 'quick'" id="mobile-quick-menu" class="bottom-popup" role="menu">
|
||||
<template v-for="item in quickNavigation" :key="item.id">
|
||||
<li v-if="'kind' in item" class="bottom-divider" role="separator"></li>
|
||||
<li v-else role="none">
|
||||
<button
|
||||
class="quick-link"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
:data-quick-id="item.id"
|
||||
@click="onQuick(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<li class="bottom-lobby" role="none">
|
||||
<button class="quick-link lobby-link" type="button" role="menuitem" @click="emit('lobby')">
|
||||
로비로
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="bottom-trigger refresh-trigger"
|
||||
type="button"
|
||||
data-bottom-menu="refresh"
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
갱신
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-mobile-bottom {
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
display: none;
|
||||
width: 500px;
|
||||
max-width: 100vw;
|
||||
height: 45px;
|
||||
grid-template-columns: repeat(4, 125px);
|
||||
transform: translateX(-50%);
|
||||
border-top: 1px solid #111;
|
||||
background: #202020;
|
||||
box-shadow: 0 -1px 0 #212529;
|
||||
}
|
||||
|
||||
.bottom-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bottom-trigger {
|
||||
box-sizing: border-box;
|
||||
width: 125px;
|
||||
height: 45px;
|
||||
border: 1px solid #1f1712;
|
||||
padding: 6px 4px;
|
||||
background: #302016 var(--sammo-texture-walnut);
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nation-trigger {
|
||||
border-color: color-mix(in srgb, var(--nation-menu-color) 85%, #000);
|
||||
background: var(--nation-menu-color);
|
||||
}
|
||||
|
||||
.quick-trigger {
|
||||
background: #212529;
|
||||
}
|
||||
|
||||
.bottom-trigger:hover,
|
||||
.bottom-trigger:focus-visible,
|
||||
.bottom-trigger[aria-expanded='true'] {
|
||||
filter: brightness(1.14);
|
||||
}
|
||||
|
||||
.bottom-trigger:active {
|
||||
filter: brightness(0.82);
|
||||
}
|
||||
|
||||
.dropup-caret {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 4px solid currentColor;
|
||||
border-left: 4px solid transparent;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bottom-popup {
|
||||
position: absolute;
|
||||
z-index: 120;
|
||||
bottom: 47px;
|
||||
left: 0;
|
||||
columns: 3;
|
||||
width: max-content;
|
||||
min-width: 375px;
|
||||
max-width: 500px;
|
||||
max-height: calc(100vh - 50px);
|
||||
margin: 0;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 3px;
|
||||
padding: 4px 0;
|
||||
overflow-y: auto;
|
||||
background: #202020;
|
||||
box-shadow: 0 -8px 18px rgb(0 0 0 / 45%);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nation-bottom-item .bottom-popup {
|
||||
left: -125px;
|
||||
}
|
||||
|
||||
.bottom-item:nth-child(3) .bottom-popup {
|
||||
right: -125px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.bottom-popup li {
|
||||
break-inside: avoid;
|
||||
min-width: 125px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.bottom-popup :deep(.main-menu-link),
|
||||
.quick-link {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 5px 16px;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bottom-popup :deep(.main-menu-link:hover),
|
||||
.bottom-popup :deep(.main-menu-link:focus-visible),
|
||||
.quick-link:hover,
|
||||
.quick-link:focus-visible {
|
||||
background: #353535;
|
||||
}
|
||||
|
||||
.bottom-heading {
|
||||
padding: 5px 16px;
|
||||
color: #888;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bottom-divider {
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.bottom-lobby {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.lobby-link {
|
||||
justify-content: center;
|
||||
background: #302016 var(--sammo-texture-walnut);
|
||||
}
|
||||
|
||||
@media (max-width: 939.98px) {
|
||||
.main-mobile-bottom {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import MainNavigationLink from './MainNavigationLink.vue';
|
||||
import {
|
||||
isNationNavigationEnabled,
|
||||
nationNavigation,
|
||||
type MainNavigationLink as MainNavigationLinkItem,
|
||||
type NationNavigationAccess,
|
||||
} from './mainNavigation';
|
||||
import { useMenuPopup } from './useMenuPopup';
|
||||
|
||||
const props = defineProps<{
|
||||
access: NationNavigationAccess;
|
||||
tournamentStage: number;
|
||||
nationColor: string;
|
||||
}>();
|
||||
|
||||
const { setRoot, openId, close, toggle } = useMenuPopup();
|
||||
const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props.tournamentStage;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
:ref="setRoot"
|
||||
class="main-nation-menu"
|
||||
:style="{ '--nation-menu-color': nationColor || '#000000' }"
|
||||
aria-label="국가 메뉴"
|
||||
>
|
||||
<template v-for="entry in nationNavigation" :key="entry.id">
|
||||
<MainNavigationLink
|
||||
v-if="entry.kind === 'link'"
|
||||
:link="entry"
|
||||
:enabled="isNationNavigationEnabled(entry, access)"
|
||||
:active="isActive(entry)"
|
||||
/>
|
||||
<div v-else-if="entry.kind === 'split'" class="nation-menu-split">
|
||||
<MainNavigationLink
|
||||
:link="entry.main"
|
||||
:enabled="isNationNavigationEnabled(entry.main, access)"
|
||||
:active="isActive(entry.main)"
|
||||
/>
|
||||
<button
|
||||
class="main-menu-button nation-menu-split__toggle"
|
||||
type="button"
|
||||
:data-menu-id="entry.id"
|
||||
:aria-label="`${entry.main.label} 하위 메뉴`"
|
||||
:aria-expanded="openId === entry.id"
|
||||
:aria-controls="`nation-menu-${entry.id}`"
|
||||
@click="toggle(entry.id, $event)"
|
||||
>
|
||||
<span class="menu-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul v-show="openId === entry.id" :id="`nation-menu-${entry.id}`" class="nation-menu-popup" role="menu">
|
||||
<li v-for="item in entry.items" :key="item.id" role="none">
|
||||
<template v-if="item.kind === 'link'">
|
||||
<MainNavigationLink
|
||||
:link="item"
|
||||
:enabled="isNationNavigationEnabled(item, access)"
|
||||
role="menuitem"
|
||||
@navigate="close()"
|
||||
/>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-nation-menu {
|
||||
position: relative;
|
||||
z-index: 29;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
gap: 1.6px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.main-nation-menu :deep(.main-menu-link),
|
||||
.main-nation-menu .main-menu-button {
|
||||
border-color: color-mix(in srgb, var(--nation-menu-color) 85%, #000);
|
||||
background-color: var(--nation-menu-color);
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.nation-menu-split {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 28px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nation-menu-split > :deep(.main-menu-link) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nation-menu-split__toggle {
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
padding: 0;
|
||||
border-left-width: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.menu-caret {
|
||||
display: inline-block;
|
||||
border-top: 4px solid currentColor;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.nation-menu-popup {
|
||||
position: absolute;
|
||||
z-index: 120;
|
||||
top: calc(100% + 2px);
|
||||
right: 0;
|
||||
min-width: 180px;
|
||||
margin: 0;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 3px;
|
||||
padding: 4px 0;
|
||||
background: #202020;
|
||||
box-shadow: 0 8px 18px rgb(0 0 0 / 45%);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nation-menu-popup :deep(.main-menu-link) {
|
||||
min-height: 30px;
|
||||
justify-content: flex-start;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 5px 16px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 939.98px) {
|
||||
.main-nation-menu {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { MainNavigationLink } from './mainNavigation';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
link: MainNavigationLink;
|
||||
enabled?: boolean;
|
||||
compact?: boolean;
|
||||
active?: boolean;
|
||||
}>(),
|
||||
{
|
||||
enabled: true,
|
||||
compact: false,
|
||||
active: false,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
navigate: [];
|
||||
}>();
|
||||
|
||||
const label = computed(() => (props.compact ? (props.link.compactLabel ?? props.link.label) : props.link.label));
|
||||
const rel = computed(() => (props.link.newTab ? 'noopener noreferrer' : undefined));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink
|
||||
v-if="enabled && link.to"
|
||||
class="main-menu-link"
|
||||
:class="{ highlight: active }"
|
||||
:to="link.to"
|
||||
:target="link.newTab ? '_blank' : undefined"
|
||||
:rel="rel"
|
||||
:data-navigation-id="link.id"
|
||||
@click="emit('navigate')"
|
||||
>
|
||||
{{ label }}
|
||||
</RouterLink>
|
||||
<a
|
||||
v-else-if="enabled && link.href"
|
||||
class="main-menu-link"
|
||||
:class="{ highlight: active }"
|
||||
:href="link.href"
|
||||
:target="link.newTab ? '_blank' : undefined"
|
||||
:rel="rel"
|
||||
:data-navigation-id="link.id"
|
||||
@click="emit('navigate')"
|
||||
>
|
||||
{{ label }}
|
||||
</a>
|
||||
<span
|
||||
v-else
|
||||
class="main-menu-link disabled"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
:title="link.unavailableReason"
|
||||
:data-navigation-id="link.id"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.main-menu-link,
|
||||
.main-menu-button {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 31px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #1f1712;
|
||||
border-radius: 3px;
|
||||
padding: 6px 12px;
|
||||
background-color: #302016;
|
||||
background-image: var(--sammo-texture-walnut);
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.main-menu-link:hover,
|
||||
.main-menu-link:focus-visible,
|
||||
.main-menu-button:hover,
|
||||
.main-menu-button:focus-visible {
|
||||
border-color: #6f5140;
|
||||
color: #fff;
|
||||
filter: brightness(1.14);
|
||||
outline: 2px solid transparent;
|
||||
}
|
||||
|
||||
.main-menu-link:active,
|
||||
.main-menu-button:active,
|
||||
.main-menu-button[aria-expanded='true'] {
|
||||
filter: brightness(0.82);
|
||||
}
|
||||
|
||||
.main-menu-link.highlight,
|
||||
.main-menu-button.highlight {
|
||||
color: magenta;
|
||||
}
|
||||
|
||||
.main-menu-link.disabled,
|
||||
.main-menu-button:disabled {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
filter: none;
|
||||
opacity: 0.65;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,357 @@
|
||||
export type NationAccessRule =
|
||||
'always' | 'meeting' | 'secret' | 'nation-member' | 'nation-established' | 'nation-secret';
|
||||
|
||||
export type MainNavigationLink = {
|
||||
kind: 'link';
|
||||
id: string;
|
||||
label: string;
|
||||
to?: string;
|
||||
href?: string;
|
||||
compactLabel?: string;
|
||||
newTab?: boolean;
|
||||
access?: NationAccessRule;
|
||||
highlightStage?: 1 | 6;
|
||||
unavailableReason?: string;
|
||||
};
|
||||
|
||||
export interface MainNavigationDivider {
|
||||
kind: 'divider';
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface MainNavigationGroup {
|
||||
kind: 'group';
|
||||
id: string;
|
||||
label: string;
|
||||
items: Array<MainNavigationLink | MainNavigationDivider>;
|
||||
}
|
||||
|
||||
export interface MainNavigationSplit {
|
||||
kind: 'split';
|
||||
id: string;
|
||||
main: MainNavigationLink;
|
||||
items: Array<MainNavigationLink | MainNavigationDivider>;
|
||||
}
|
||||
|
||||
export type MainNavigationEntry = MainNavigationLink | MainNavigationGroup | MainNavigationSplit;
|
||||
|
||||
export interface NationNavigationAccess {
|
||||
permission: number;
|
||||
officerLevel: number;
|
||||
nationLevel: number;
|
||||
}
|
||||
|
||||
export interface QuickNavigationItem {
|
||||
id: string;
|
||||
label: string;
|
||||
tab: 'map' | 'commands' | 'status' | 'world' | 'messages';
|
||||
selector: string;
|
||||
}
|
||||
|
||||
const configuredExternalLink = (
|
||||
id: string,
|
||||
label: string,
|
||||
value: string | undefined,
|
||||
unavailableReason: string
|
||||
): MainNavigationLink => {
|
||||
const href = value?.trim();
|
||||
return {
|
||||
kind: 'link',
|
||||
id,
|
||||
label,
|
||||
...(href ? { href } : {}),
|
||||
newTab: true,
|
||||
unavailableReason: href ? undefined : unavailableReason,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildGlobalNavigation = (npcMode: number): MainNavigationEntry[] => [
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'nation-betting',
|
||||
label: '천통국 베팅',
|
||||
to: '/nation-betting',
|
||||
},
|
||||
{
|
||||
kind: 'group',
|
||||
id: 'game-info',
|
||||
label: '게임정보',
|
||||
items: [
|
||||
{ kind: 'link', id: 'nation-list', label: '세력일람', to: '/nation-list', newTab: true },
|
||||
{ kind: 'link', id: 'general-list', label: '장수일람', to: '/general-list', newTab: true },
|
||||
{ kind: 'link', id: 'best-general', label: '명장일람', to: '/best-general', newTab: true },
|
||||
{ kind: 'divider', id: 'game-info-divider' },
|
||||
{ kind: 'link', id: 'hall-of-fame', label: '명예의전당', to: '/hall-of-fame', newTab: true },
|
||||
{ kind: 'link', id: 'dynasty', label: '왕조일람', to: '/dynasty', newTab: true },
|
||||
],
|
||||
},
|
||||
{ kind: 'link', id: 'yearbook', label: '연감', to: '/yearbook', newTab: true },
|
||||
{
|
||||
kind: 'split',
|
||||
id: 'boards',
|
||||
main: {
|
||||
kind: 'link',
|
||||
id: 'board-community',
|
||||
label: '게시판',
|
||||
href: import.meta.env.VITE_BOARD_COMMUNITY_URL?.trim() || '/xe/community',
|
||||
newTab: true,
|
||||
},
|
||||
items: [
|
||||
configuredExternalLink(
|
||||
'board-request',
|
||||
'건의/제안',
|
||||
import.meta.env.VITE_BOARD_REQUEST_URL,
|
||||
'건의/제안 게시판 URL이 설정되지 않았습니다.'
|
||||
),
|
||||
configuredExternalLink(
|
||||
'board-tip',
|
||||
'팁/강좌',
|
||||
import.meta.env.VITE_BOARD_TIP_URL,
|
||||
'팁/강좌 게시판 URL이 설정되지 않았습니다.'
|
||||
),
|
||||
{ kind: 'divider', id: 'board-divider' },
|
||||
configuredExternalLink(
|
||||
'board-patch',
|
||||
'패치 내역',
|
||||
import.meta.env.VITE_BOARD_PATCH_URL,
|
||||
'패치 내역 URL이 설정되지 않았습니다.'
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'split',
|
||||
id: 'open-chat',
|
||||
main: configuredExternalLink(
|
||||
'official-chat',
|
||||
'공식 오픈 톡',
|
||||
import.meta.env.VITE_OFFICIAL_CHAT_URL,
|
||||
'공식 오픈톡 URL이 설정되지 않았습니다.'
|
||||
),
|
||||
items: [
|
||||
configuredExternalLink(
|
||||
'casual-chat',
|
||||
'잡담 오픈 톡',
|
||||
import.meta.env.VITE_CASUAL_CHAT_URL,
|
||||
'잡담 오픈톡 URL이 설정되지 않았습니다.'
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'battle-simulator',
|
||||
label: '전투 시뮬레이터',
|
||||
to: '/battle-simulator',
|
||||
newTab: true,
|
||||
},
|
||||
{
|
||||
kind: 'group',
|
||||
id: 'other-info',
|
||||
label: '기타 정보',
|
||||
items: [
|
||||
{ kind: 'link', id: 'traffic', label: '접속량정보', to: '/traffic', newTab: true },
|
||||
...(npcMode > 0
|
||||
? [{ kind: 'link', id: 'npc-list', label: '빙의일람', to: '/npc-list', newTab: true } as const]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{ kind: 'link', id: 'survey', label: '설문조사', to: '/survey', newTab: true },
|
||||
];
|
||||
|
||||
export const nationNavigation: MainNavigationEntry[] = [
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'meeting',
|
||||
label: '회 의 실',
|
||||
compactLabel: '회의실',
|
||||
to: '/board',
|
||||
access: 'meeting',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'secret-board',
|
||||
label: '기 밀 실',
|
||||
compactLabel: '기밀실',
|
||||
to: '/board/secret',
|
||||
access: 'secret',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'troop',
|
||||
label: '부대 편성',
|
||||
to: '/troop',
|
||||
access: 'nation-established',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'diplomacy',
|
||||
label: '외 교 부',
|
||||
compactLabel: '외교부',
|
||||
to: '/diplomacy',
|
||||
access: 'nation-secret',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'personnel',
|
||||
label: '인 사 부',
|
||||
compactLabel: '인사부',
|
||||
to: '/nation/personnel',
|
||||
access: 'nation-member',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'finance',
|
||||
label: '내 무 부',
|
||||
compactLabel: '내무부',
|
||||
to: '/nation/finance',
|
||||
access: 'nation-secret',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'chief-center',
|
||||
label: '사 령 부',
|
||||
compactLabel: '사령부',
|
||||
to: '/chief-center',
|
||||
access: 'nation-secret',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'npc-control',
|
||||
label: 'NPC 정책',
|
||||
to: '/npc-control',
|
||||
access: 'nation-secret',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'nation-secret',
|
||||
label: '암 행 부',
|
||||
compactLabel: '암행부',
|
||||
to: '/nation/secret',
|
||||
newTab: true,
|
||||
access: 'nation-secret',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'tournament',
|
||||
label: '토 너 먼 트',
|
||||
compactLabel: '토너먼트',
|
||||
to: '/tournament',
|
||||
newTab: true,
|
||||
access: 'always',
|
||||
highlightStage: 1,
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'nation-info',
|
||||
label: '세력 정보',
|
||||
to: '/nation/info',
|
||||
access: 'nation-member',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'nation-cities',
|
||||
label: '세력 도시',
|
||||
to: '/nation/cities',
|
||||
access: 'nation-established',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'nation-generals',
|
||||
label: '세력 장수',
|
||||
to: '/nation/generals',
|
||||
access: 'nation-member',
|
||||
},
|
||||
{ kind: 'link', id: 'global-info', label: '중원 정보', to: '/global-info', access: 'always' },
|
||||
{ kind: 'link', id: 'current-city', label: '현재 도시', to: '/current-city', access: 'always' },
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'battle-center',
|
||||
label: '감 찰 부',
|
||||
compactLabel: '감찰부',
|
||||
to: '/battle-center',
|
||||
newTab: true,
|
||||
access: 'nation-secret',
|
||||
},
|
||||
{ kind: 'link', id: 'inherit', label: '유산 관리', to: '/inherit', access: 'always' },
|
||||
{ kind: 'link', id: 'my-page', label: '내 정보&설정', to: '/my-page', access: 'always' },
|
||||
{
|
||||
kind: 'split',
|
||||
id: 'auction',
|
||||
main: {
|
||||
kind: 'link',
|
||||
id: 'auction-resource',
|
||||
label: '경 매 장',
|
||||
compactLabel: '금/쌀 경매장',
|
||||
to: '/auction',
|
||||
newTab: true,
|
||||
access: 'always',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'auction-resource-menu',
|
||||
label: '금/쌀 경매장',
|
||||
to: '/auction',
|
||||
newTab: true,
|
||||
access: 'always',
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'auction-unique',
|
||||
label: '유니크 경매장',
|
||||
to: '/auction?type=unique',
|
||||
newTab: true,
|
||||
access: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'link',
|
||||
id: 'betting',
|
||||
label: '베 팅 장',
|
||||
compactLabel: '베팅장',
|
||||
to: '/betting',
|
||||
newTab: true,
|
||||
access: 'always',
|
||||
highlightStage: 6,
|
||||
},
|
||||
];
|
||||
|
||||
export const quickNavigation: Array<QuickNavigationItem | MainNavigationDivider> = [
|
||||
{ kind: 'divider', id: 'quick-nation-heading' },
|
||||
{ id: 'policy', label: '방침', tab: 'map', selector: '[data-main-target="policy"]' },
|
||||
{ id: 'commands', label: '명령', tab: 'commands', selector: '[data-main-target="commands"]' },
|
||||
{ id: 'nation', label: '국가', tab: 'status', selector: '[data-main-target="nation"]' },
|
||||
{ id: 'general', label: '장수', tab: 'status', selector: '[data-main-target="general"]' },
|
||||
{ id: 'city', label: '도시', tab: 'status', selector: '[data-main-target="city"]' },
|
||||
{ kind: 'divider', id: 'quick-record-heading' },
|
||||
{ id: 'map', label: '지도', tab: 'map', selector: '[data-main-target="map"]' },
|
||||
{ id: 'global-records', label: '동향', tab: 'world', selector: '[data-main-target="global-records"]' },
|
||||
{ id: 'general-records', label: '개인', tab: 'world', selector: '[data-main-target="general-records"]' },
|
||||
{ id: 'world-history', label: '정세', tab: 'world', selector: '[data-main-target="world-history"]' },
|
||||
{ kind: 'divider', id: 'quick-message-heading' },
|
||||
{ id: 'public-message', label: '전체', tab: 'messages', selector: '[data-message-type="public"]' },
|
||||
{ id: 'national-message', label: '국가', tab: 'messages', selector: '[data-message-type="national"]' },
|
||||
{ id: 'private-message', label: '개인', tab: 'messages', selector: '[data-message-type="private"]' },
|
||||
{ id: 'diplomacy-message', label: '외교', tab: 'messages', selector: '[data-message-type="diplomacy"]' },
|
||||
];
|
||||
|
||||
export const isNavigationConfigured = (link: MainNavigationLink): boolean => Boolean(link.to || link.href);
|
||||
|
||||
export const isNationNavigationEnabled = (link: MainNavigationLink, access: NationNavigationAccess): boolean => {
|
||||
const rule = link.access ?? 'always';
|
||||
const showSecret = access.permission >= 1 || access.officerLevel >= 2;
|
||||
switch (rule) {
|
||||
case 'meeting':
|
||||
return access.officerLevel >= 1;
|
||||
case 'secret':
|
||||
return access.permission >= 2;
|
||||
case 'nation-member':
|
||||
return access.officerLevel >= 1;
|
||||
case 'nation-established':
|
||||
return access.officerLevel >= 1 && access.nationLevel >= 1;
|
||||
case 'nation-secret':
|
||||
return showSecret;
|
||||
case 'always':
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
export const useMenuPopup = () => {
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
const openId = ref<string | null>(null);
|
||||
let trigger: HTMLElement | null = null;
|
||||
|
||||
const close = (restoreFocus = false) => {
|
||||
openId.value = null;
|
||||
if (restoreFocus && trigger) {
|
||||
void nextTick(() => trigger?.focus());
|
||||
}
|
||||
};
|
||||
|
||||
const toggle = (id: string, event: MouseEvent) => {
|
||||
const nextOpen = openId.value === id ? null : id;
|
||||
trigger = nextOpen ? (event.currentTarget as HTMLElement) : null;
|
||||
openId.value = nextOpen;
|
||||
};
|
||||
|
||||
const setRoot = (element: unknown) => {
|
||||
root.value = element instanceof HTMLElement ? element : null;
|
||||
};
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
const target = event.target;
|
||||
if (target instanceof Node && !root.value?.contains(target)) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && openId.value) {
|
||||
event.preventDefault();
|
||||
close(true);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', onPointerDown);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('pointerdown', onPointerDown);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
});
|
||||
|
||||
return { setRoot, openId, close, toggle };
|
||||
};
|
||||
Reference in New Issue
Block a user