feat(nav): add collapsible sidebar and nested navigation groups
Refactor sidebar to support nested navigation groups for related entities. Implement collapsible desktop sidebar with icon tooltips.
This commit is contained in:
@@ -5,8 +5,9 @@
|
|||||||
- Pokopia Wiki 是一个面向 Pokopia 游戏资料的社区 Wiki。
|
- Pokopia Wiki 是一个面向 Pokopia 游戏资料的社区 Wiki。
|
||||||
- 所有人都可以浏览 Wiki 内容。
|
- 所有人都可以浏览 Wiki 内容。
|
||||||
- 已注册并完成邮箱验证且拥有对应权限的用户可以创建、编辑、删除 Wiki 内容。
|
- 已注册并完成邮箱验证且拥有对应权限的用户可以创建、编辑、删除 Wiki 内容。
|
||||||
- 前台以 Home 首页、Pokemon、Event Pokemon、栖息地、Event Habitats、物品、Event Items、Ancient Artifacts、材料单、每日 CheckList、Life、Automation、Dish、Events、Actions、Dream Island、Clothes 为主要浏览入口。
|
- 前台以 Home 首页、Pokedex(Main Game / Event)、Habitat Dex(Main Game / Event)、Collections(Main Game / Event / Ancient Artifacts)、材料单、每日 CheckList、Life、Automation、Dish、Events、Actions、Dream Island、Clothes 为主要浏览入口。
|
||||||
- Home 首页路径为 `/`,用于聚合公开 Wiki 入口;Logo 导航回到 Home,用户可从 Home 进入核心资料、每日 CheckList、Life 和正在准备中的分区。
|
- Home 首页路径为 `/`,用于聚合公开 Wiki 入口;Logo 导航回到 Home,用户可从 Home 进入核心资料、每日 CheckList、Life 和正在准备中的分区。
|
||||||
|
- 桌面端使用侧边栏导航,侧边栏可折叠为图标栏;移动端继续使用抽屉式侧边栏。
|
||||||
- 管理入口用于维护全局配置、语言、系统文案、列表排序和每日 CheckList。
|
- 管理入口用于维护全局配置、语言、系统文案、列表排序和每日 CheckList。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
iconItem,
|
iconItem,
|
||||||
iconLife,
|
iconLife,
|
||||||
iconPokemon,
|
iconPokemon,
|
||||||
iconRecipe
|
iconRecipe,
|
||||||
|
type AppIcon
|
||||||
} from './icons';
|
} from './icons';
|
||||||
import { getCurrentLocale, loadSystemWordings, onLocaleChange, setCurrentLocale } from './i18n';
|
import { getCurrentLocale, loadSystemWordings, onLocaleChange, setCurrentLocale } from './i18n';
|
||||||
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './services/api';
|
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './services/api';
|
||||||
@@ -34,24 +35,66 @@ const languages = ref<Language[]>([
|
|||||||
let removeAuthListener: (() => void) | null = null;
|
let removeAuthListener: (() => void) | null = null;
|
||||||
let removeLocaleListener: (() => void) | null = null;
|
let removeLocaleListener: (() => void) | null = null;
|
||||||
|
|
||||||
function inDevBadge() {
|
type NavBadge = {
|
||||||
return { label: t('common.inDev'), tone: 'info' as const };
|
label: string;
|
||||||
|
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavLinkItem = {
|
||||||
|
label: string;
|
||||||
|
to: string;
|
||||||
|
icon?: AppIcon;
|
||||||
|
badge?: NavBadge;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavGroupItem = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
icon?: AppIcon;
|
||||||
|
children: NavLinkItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavItem = NavLinkItem | NavGroupItem;
|
||||||
|
|
||||||
|
function inDevBadge(): NavBadge {
|
||||||
|
return { label: t('common.inDev'), tone: 'info' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function can(permissionKey: string) {
|
function can(permissionKey: string) {
|
||||||
return currentUser.value?.permissions.includes(permissionKey) === true;
|
return currentUser.value?.permissions.includes(permissionKey) === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = computed(() => {
|
const navItems = computed<NavItem[]>(() => {
|
||||||
const items = [
|
const items: NavItem[] = [
|
||||||
{ label: t('nav.home'), to: '/', icon: iconHome },
|
{ label: t('nav.home'), to: '/', icon: iconHome },
|
||||||
{ label: t('nav.pokemon'), to: '/pokemon', icon: iconPokemon },
|
{
|
||||||
{ label: t('nav.eventPokemon'), to: '/event-pokemon', icon: iconEvent },
|
key: 'pokedex',
|
||||||
{ label: t('nav.habitats'), to: '/habitats', icon: iconHabitat },
|
label: t('nav.pokedex'),
|
||||||
{ label: t('nav.eventHabitats'), to: '/event-habitats', icon: iconEvent },
|
icon: iconPokemon,
|
||||||
{ label: t('nav.items'), to: '/items', icon: iconItem },
|
children: [
|
||||||
{ label: t('nav.eventItems'), to: '/event-items', icon: iconEvent },
|
{ label: t('nav.mainGame'), to: '/pokemon', icon: iconPokemon },
|
||||||
{ label: t('nav.ancientArtifacts'), to: '/ancient-artifacts', icon: iconArtifact },
|
{ label: t('nav.event'), to: '/event-pokemon', icon: iconEvent }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'habitat-dex',
|
||||||
|
label: t('nav.habitatDex'),
|
||||||
|
icon: iconHabitat,
|
||||||
|
children: [
|
||||||
|
{ label: t('nav.mainGame'), to: '/habitats', icon: iconHabitat },
|
||||||
|
{ label: t('nav.event'), to: '/event-habitats', icon: iconEvent }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'collections',
|
||||||
|
label: t('nav.collections'),
|
||||||
|
icon: iconItem,
|
||||||
|
children: [
|
||||||
|
{ label: t('nav.mainGame'), to: '/items', icon: iconItem },
|
||||||
|
{ label: t('nav.event'), to: '/event-items', icon: iconEvent },
|
||||||
|
{ label: t('nav.ancientArtifacts'), to: '/ancient-artifacts', icon: iconArtifact }
|
||||||
|
]
|
||||||
|
},
|
||||||
{ label: t('nav.recipes'), to: '/recipes', icon: iconRecipe },
|
{ label: t('nav.recipes'), to: '/recipes', icon: iconRecipe },
|
||||||
{ label: t('nav.automation'), to: '/automation', icon: iconAutomation, badge: inDevBadge() },
|
{ label: t('nav.automation'), to: '/automation', icon: iconAutomation, badge: inDevBadge() },
|
||||||
{ label: t('nav.dish'), to: '/dish', icon: iconDish, badge: inDevBadge() },
|
{ label: t('nav.dish'), to: '/dish', icon: iconDish, badge: inDevBadge() },
|
||||||
|
|||||||
@@ -3,24 +3,54 @@ import { Icon } from '@iconify/vue';
|
|||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { iconClose, iconLogin, iconLogout, iconMenu, iconProfile, iconRegister, iconTranslate, type AppIcon } from '../icons';
|
import {
|
||||||
|
iconChevronDown,
|
||||||
|
iconChevronRight,
|
||||||
|
iconClose,
|
||||||
|
iconLogin,
|
||||||
|
iconLogout,
|
||||||
|
iconMenu,
|
||||||
|
iconProfile,
|
||||||
|
iconRegister,
|
||||||
|
iconTranslate,
|
||||||
|
type AppIcon
|
||||||
|
} from '../icons';
|
||||||
import type { AuthUser, Language } from '../services/api';
|
import type { AuthUser, Language } from '../services/api';
|
||||||
import PokeBallMark from './PokeBallMark.vue';
|
import PokeBallMark from './PokeBallMark.vue';
|
||||||
import StatusBadge from './StatusBadge.vue';
|
import StatusBadge from './StatusBadge.vue';
|
||||||
|
|
||||||
|
type NavBadge = {
|
||||||
|
label: string;
|
||||||
|
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavLinkItem = {
|
||||||
|
label: string;
|
||||||
|
to: string;
|
||||||
|
icon?: AppIcon;
|
||||||
|
badge?: NavBadge;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavGroupItem = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
icon?: AppIcon;
|
||||||
|
children: NavLinkItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavItem = NavLinkItem | NavGroupItem;
|
||||||
|
|
||||||
|
type SidebarTooltip = {
|
||||||
|
label: string;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
currentUser: AuthUser | null;
|
currentUser: AuthUser | null;
|
||||||
languages: Language[];
|
languages: Language[];
|
||||||
locale: string;
|
locale: string;
|
||||||
navItems: Array<{
|
navItems: NavItem[];
|
||||||
label: string;
|
|
||||||
to: string;
|
|
||||||
icon?: AppIcon;
|
|
||||||
badge?: {
|
|
||||||
label: string;
|
|
||||||
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -33,25 +63,61 @@ const route = useRoute();
|
|||||||
const copyrightYear = new Date().getFullYear();
|
const copyrightYear = new Date().getFullYear();
|
||||||
const languageMenu = ref<HTMLElement | null>(null);
|
const languageMenu = ref<HTMLElement | null>(null);
|
||||||
const languageMenuButton = ref<HTMLButtonElement | null>(null);
|
const languageMenuButton = ref<HTMLButtonElement | null>(null);
|
||||||
|
const sideNav = ref<HTMLElement | null>(null);
|
||||||
const languageMenuOpen = ref(false);
|
const languageMenuOpen = ref(false);
|
||||||
const sidebarOpen = ref(false);
|
const sidebarOpen = ref(false);
|
||||||
|
const sidebarCollapsed = ref(false);
|
||||||
|
const expandedNavGroups = ref<Set<string>>(new Set());
|
||||||
|
const sidebarTooltip = ref<SidebarTooltip | null>(null);
|
||||||
|
const sidebarTooltipTarget = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
function closeLanguageMenu() {
|
function closeLanguageMenu() {
|
||||||
languageMenuOpen.value = false;
|
languageMenuOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearSidebarTooltipTarget() {
|
||||||
|
sidebarTooltipTarget.value?.removeAttribute('aria-describedby');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSidebarTooltip() {
|
||||||
|
clearSidebarTooltipTarget();
|
||||||
|
sidebarTooltipTarget.value = null;
|
||||||
|
sidebarTooltip.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
function closeSidebar() {
|
function closeSidebar() {
|
||||||
sidebarOpen.value = false;
|
sidebarOpen.value = false;
|
||||||
closeLanguageMenu();
|
closeLanguageMenu();
|
||||||
|
hideSidebarTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
sidebarOpen.value = !sidebarOpen.value;
|
sidebarOpen.value = !sidebarOpen.value;
|
||||||
closeLanguageMenu();
|
closeLanguageMenu();
|
||||||
|
hideSidebarTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSidebarCollapsed() {
|
||||||
|
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||||
|
closeLanguageMenu();
|
||||||
|
hideSidebarTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNavGroup(key: string) {
|
||||||
|
const nextGroups = new Set(expandedNavGroups.value);
|
||||||
|
if (nextGroups.has(key)) {
|
||||||
|
nextGroups.delete(key);
|
||||||
|
} else {
|
||||||
|
nextGroups.add(key);
|
||||||
|
}
|
||||||
|
expandedNavGroups.value = nextGroups;
|
||||||
|
closeLanguageMenu();
|
||||||
|
hideSidebarTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLanguageMenu() {
|
function toggleLanguageMenu() {
|
||||||
languageMenuOpen.value = !languageMenuOpen.value;
|
languageMenuOpen.value = !languageMenuOpen.value;
|
||||||
|
hideSidebarTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLocale(value: string) {
|
function selectLocale(value: string) {
|
||||||
@@ -79,26 +145,108 @@ function requestLogout() {
|
|||||||
emit('logout');
|
emit('logout');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDesktopSidebar() {
|
||||||
|
return typeof window !== 'undefined' && window.matchMedia('(min-width: 901px)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canShowSidebarTooltip(collapsedOnly = true) {
|
||||||
|
return isDesktopSidebar() && (!collapsedOnly || sidebarCollapsed.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSidebarTooltip(label: string, target: HTMLElement) {
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
clearSidebarTooltipTarget();
|
||||||
|
sidebarTooltipTarget.value = target;
|
||||||
|
target.setAttribute('aria-describedby', 'sidebar-tooltip');
|
||||||
|
sidebarTooltip.value = {
|
||||||
|
label,
|
||||||
|
top: rect.top + rect.height / 2,
|
||||||
|
left: rect.right + 10
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSidebarTooltip(label: string, event: MouseEvent | FocusEvent, collapsedOnly = true) {
|
||||||
|
if (!canShowSidebarTooltip(collapsedOnly) || languageMenuOpen.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.currentTarget;
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
setSidebarTooltip(label, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSidebarTooltipPosition() {
|
||||||
|
const target = sidebarTooltipTarget.value;
|
||||||
|
const currentTooltip = sidebarTooltip.value;
|
||||||
|
if (!target || !currentTooltip || !canShowSidebarTooltip()) {
|
||||||
|
hideSidebarTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sideNav.value?.contains(target)) {
|
||||||
|
const navRect = sideNav.value.getBoundingClientRect();
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
if (targetRect.bottom < navRect.top || targetRect.top > navRect.bottom) {
|
||||||
|
hideSidebarTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSidebarTooltip(currentTooltip.label, target);
|
||||||
|
}
|
||||||
|
|
||||||
function isNavActive(path: string) {
|
function isNavActive(path: string) {
|
||||||
return route.path === path || route.path.startsWith(`${path}/`);
|
return route.path === path || route.path.startsWith(`${path}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNavGroup(item: NavItem): item is NavGroupItem {
|
||||||
|
return 'children' in item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNavGroupActive(item: NavGroupItem) {
|
||||||
|
return item.children.some((child) => isNavActive(child.to));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNavGroupExpanded(item: NavGroupItem) {
|
||||||
|
return expandedNavGroups.value.has(item.key) || isNavGroupActive(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navItemKey(item: NavItem) {
|
||||||
|
return isNavGroup(item) ? item.key : item.to;
|
||||||
|
}
|
||||||
|
|
||||||
watch(sidebarOpen, (open) => {
|
watch(sidebarOpen, (open) => {
|
||||||
document.body.classList.toggle('lock-scroll', open);
|
document.body.classList.toggle('lock-scroll', open);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(sidebarCollapsed, (collapsed) => {
|
||||||
|
if (!collapsed) {
|
||||||
|
hideSidebarTooltip();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('pointerdown', onDocumentPointerDown);
|
document.addEventListener('pointerdown', onDocumentPointerDown);
|
||||||
|
window.addEventListener('resize', updateSidebarTooltipPosition);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('pointerdown', onDocumentPointerDown);
|
document.removeEventListener('pointerdown', onDocumentPointerDown);
|
||||||
|
window.removeEventListener('resize', updateSidebarTooltipPosition);
|
||||||
document.body.classList.remove('lock-scroll');
|
document.body.classList.remove('lock-scroll');
|
||||||
|
hideSidebarTooltip();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-shell" :class="{ 'app-shell--sidebar-open': sidebarOpen }">
|
<div
|
||||||
|
class="app-shell"
|
||||||
|
:class="{
|
||||||
|
'app-shell--sidebar-open': sidebarOpen,
|
||||||
|
'app-shell--sidebar-collapsed': sidebarCollapsed
|
||||||
|
}"
|
||||||
|
>
|
||||||
<header class="mobile-topbar">
|
<header class="mobile-topbar">
|
||||||
<button
|
<button
|
||||||
class="sidebar-toggle"
|
class="sidebar-toggle"
|
||||||
@@ -124,33 +272,110 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<aside id="app-sidebar" class="site-sidebar" :aria-label="t('nav.main')">
|
<aside id="app-sidebar" class="site-sidebar" :aria-label="t('nav.main')">
|
||||||
<div class="site-sidebar__inner">
|
<div class="site-sidebar__inner">
|
||||||
<RouterLink class="brand-lockup" to="/" aria-label="Pokopia Wiki" @click="closeSidebar">
|
<div class="site-sidebar__header">
|
||||||
<PokeBallMark size="42px" />
|
<RouterLink class="brand-lockup" to="/" aria-label="Pokopia Wiki" @click="closeSidebar">
|
||||||
<span>
|
<PokeBallMark size="42px" />
|
||||||
<span class="pokemon-word">Pokopia</span>
|
<span>
|
||||||
<span class="brand-subtitle">Community Wiki</span>
|
<span class="pokemon-word">Pokopia</span>
|
||||||
</span>
|
<span class="brand-subtitle">Community Wiki</span>
|
||||||
</RouterLink>
|
</span>
|
||||||
|
|
||||||
<nav class="side-nav" :aria-label="t('nav.main')">
|
|
||||||
<RouterLink
|
|
||||||
v-for="item in navItems"
|
|
||||||
:key="item.to"
|
|
||||||
class="side-nav__link"
|
|
||||||
:class="{ 'router-link-active': isNavActive(item.to) }"
|
|
||||||
:to="item.to"
|
|
||||||
@click="closeSidebar"
|
|
||||||
>
|
|
||||||
<Icon v-if="item.icon" :icon="item.icon" class="ui-icon side-nav__icon" aria-hidden="true" />
|
|
||||||
<span class="side-nav__label">{{ item.label }}</span>
|
|
||||||
<StatusBadge
|
|
||||||
v-if="item.badge"
|
|
||||||
class="side-nav__badge"
|
|
||||||
:label="item.badge.label"
|
|
||||||
:tone="item.badge.tone"
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="sidebar-collapse-toggle"
|
||||||
|
type="button"
|
||||||
|
:aria-label="sidebarCollapsed ? t('nav.expandSidebar') : t('nav.collapseSidebar')"
|
||||||
|
:aria-expanded="!sidebarCollapsed"
|
||||||
|
aria-controls="app-sidebar"
|
||||||
|
@focus="showSidebarTooltip(sidebarCollapsed ? t('nav.expandSidebar') : t('nav.collapseSidebar'), $event, false)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(sidebarCollapsed ? t('nav.expandSidebar') : t('nav.collapseSidebar'), $event, false)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="toggleSidebarCollapsed"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:icon="iconChevronRight"
|
||||||
|
class="ui-icon sidebar-collapse-toggle__icon"
|
||||||
|
:class="{ 'sidebar-collapse-toggle__icon--expanded': !sidebarCollapsed }"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav ref="sideNav" class="side-nav" :aria-label="t('nav.main')" @scroll="updateSidebarTooltipPosition">
|
||||||
|
<template v-for="item in navItems" :key="navItemKey(item)">
|
||||||
|
<div v-if="isNavGroup(item)" class="side-nav__group" :class="{ 'side-nav__group--active': isNavGroupActive(item) }">
|
||||||
|
<button
|
||||||
|
class="side-nav__link side-nav__group-trigger"
|
||||||
|
:class="{ 'router-link-active': isNavGroupActive(item) }"
|
||||||
|
type="button"
|
||||||
|
:aria-expanded="isNavGroupExpanded(item)"
|
||||||
|
:aria-controls="`side-nav-group-${item.key}`"
|
||||||
|
:aria-label="item.label"
|
||||||
|
@focus="showSidebarTooltip(item.label, $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(item.label, $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="toggleNavGroup(item.key)"
|
||||||
|
>
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="ui-icon side-nav__icon" aria-hidden="true" />
|
||||||
|
<span class="side-nav__label">{{ item.label }}</span>
|
||||||
|
<Icon
|
||||||
|
:icon="isNavGroupExpanded(item) ? iconChevronDown : iconChevronRight"
|
||||||
|
class="ui-icon side-nav__chevron"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="isNavGroupExpanded(item)" :id="`side-nav-group-${item.key}`" class="side-nav__children">
|
||||||
|
<RouterLink
|
||||||
|
v-for="child in item.children"
|
||||||
|
:key="child.to"
|
||||||
|
class="side-nav__link side-nav__link--child"
|
||||||
|
:class="{ 'router-link-active': isNavActive(child.to) }"
|
||||||
|
:to="child.to"
|
||||||
|
:aria-label="child.label"
|
||||||
|
@focus="showSidebarTooltip(child.label, $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(child.label, $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
|
<Icon v-if="child.icon" :icon="child.icon" class="ui-icon side-nav__icon" aria-hidden="true" />
|
||||||
|
<span class="side-nav__label">{{ child.label }}</span>
|
||||||
|
<StatusBadge
|
||||||
|
v-if="child.badge"
|
||||||
|
class="side-nav__badge"
|
||||||
|
:label="child.badge.label"
|
||||||
|
:tone="child.badge.tone"
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RouterLink
|
||||||
|
v-else
|
||||||
|
class="side-nav__link"
|
||||||
|
:class="{ 'router-link-active': isNavActive(item.to) }"
|
||||||
|
:to="item.to"
|
||||||
|
:aria-label="item.label"
|
||||||
|
@focus="showSidebarTooltip(item.label, $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(item.label, $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="ui-icon side-nav__icon" aria-hidden="true" />
|
||||||
|
<span class="side-nav__label">{{ item.label }}</span>
|
||||||
|
<StatusBadge
|
||||||
|
v-if="item.badge"
|
||||||
|
class="side-nav__badge"
|
||||||
|
:label="item.badge.label"
|
||||||
|
:tone="item.badge.tone"
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="auth-actions">
|
<div class="auth-actions">
|
||||||
@@ -162,6 +387,10 @@ onBeforeUnmount(() => {
|
|||||||
:aria-label="t('nav.language')"
|
:aria-label="t('nav.language')"
|
||||||
:aria-expanded="languageMenuOpen"
|
:aria-expanded="languageMenuOpen"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
|
@focus="showSidebarTooltip(t('nav.language'), $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(t('nav.language'), $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
@click="toggleLanguageMenu"
|
@click="toggleLanguageMenu"
|
||||||
>
|
>
|
||||||
<Icon :icon="iconTranslate" class="language-menu__icon" aria-hidden="true" />
|
<Icon :icon="iconTranslate" class="language-menu__icon" aria-hidden="true" />
|
||||||
@@ -185,29 +414,75 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="currentUser">
|
<template v-if="currentUser">
|
||||||
<RouterLink class="auth-user" to="/profile" :aria-label="t('nav.profile')" @click="closeSidebar">
|
<RouterLink
|
||||||
|
class="auth-user"
|
||||||
|
to="/profile"
|
||||||
|
:aria-label="t('nav.profile')"
|
||||||
|
@focus="showSidebarTooltip(t('nav.profile'), $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(t('nav.profile'), $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
<Icon :icon="iconProfile" class="ui-icon auth-user__icon" aria-hidden="true" />
|
<Icon :icon="iconProfile" class="ui-icon auth-user__icon" aria-hidden="true" />
|
||||||
<span class="auth-user__name">{{ currentUser.displayName || currentUser.email }}</span>
|
<span class="auth-user__name">{{ currentUser.displayName || currentUser.email }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<button class="ui-button ui-button--ghost ui-button--small" type="button" @click="requestLogout">
|
<button
|
||||||
|
class="ui-button ui-button--ghost ui-button--small"
|
||||||
|
type="button"
|
||||||
|
:aria-label="t('nav.logout')"
|
||||||
|
@focus="showSidebarTooltip(t('nav.logout'), $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(t('nav.logout'), $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="requestLogout"
|
||||||
|
>
|
||||||
<Icon :icon="iconLogout" class="ui-icon" aria-hidden="true" />
|
<Icon :icon="iconLogout" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('nav.logout') }}
|
<span class="auth-actions__label">{{ t('nav.logout') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<RouterLink class="ui-button ui-button--ghost ui-button--small" to="/login" @click="closeSidebar">
|
<RouterLink
|
||||||
|
class="ui-button ui-button--ghost ui-button--small"
|
||||||
|
to="/login"
|
||||||
|
:aria-label="t('nav.login')"
|
||||||
|
@focus="showSidebarTooltip(t('nav.login'), $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(t('nav.login'), $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
<Icon :icon="iconLogin" class="ui-icon" aria-hidden="true" />
|
<Icon :icon="iconLogin" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('nav.login') }}
|
<span class="auth-actions__label">{{ t('nav.login') }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/register" @click="closeSidebar">
|
<RouterLink
|
||||||
|
class="ui-button ui-button--primary ui-button--small"
|
||||||
|
to="/register"
|
||||||
|
:aria-label="t('nav.register')"
|
||||||
|
@focus="showSidebarTooltip(t('nav.register'), $event)"
|
||||||
|
@blur="hideSidebarTooltip"
|
||||||
|
@pointerenter="showSidebarTooltip(t('nav.register'), $event)"
|
||||||
|
@pointerleave="hideSidebarTooltip"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
<Icon :icon="iconRegister" class="ui-icon" aria-hidden="true" />
|
<Icon :icon="iconRegister" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('nav.register') }}
|
<span class="auth-actions__label">{{ t('nav.register') }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="sidebarTooltip"
|
||||||
|
id="sidebar-tooltip"
|
||||||
|
class="sidebar-tooltip"
|
||||||
|
role="tooltip"
|
||||||
|
:style="{ top: `${sidebarTooltip.top}px`, left: `${sidebarTooltip.left}px` }"
|
||||||
|
>
|
||||||
|
{{ sidebarTooltip.label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="page">
|
<main class="page">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ svg {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 252px minmax(0, 1fr);
|
grid-template-columns: 252px minmax(0, 1fr);
|
||||||
|
transition: grid-template-columns 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -141,6 +142,14 @@ svg {
|
|||||||
padding: 18px 14px;
|
padding: 18px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-sidebar__header {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.brand-lockup {
|
.brand-lockup {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -149,6 +158,44 @@ svg {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-sidebar__header .brand-lockup {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-toggle {
|
||||||
|
width: 38px;
|
||||||
|
min-width: 38px;
|
||||||
|
min-height: 38px;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 0.14s ease,
|
||||||
|
border-color 0.14s ease,
|
||||||
|
color 0.14s ease,
|
||||||
|
transform 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-toggle:hover {
|
||||||
|
border-color: var(--pokemon-blue);
|
||||||
|
background: rgba(255, 203, 5, 0.22);
|
||||||
|
color: var(--pokemon-blue-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-toggle__icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transition: transform 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-toggle__icon--expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
.pokemon-word {
|
.pokemon-word {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--pokemon-yellow);
|
color: var(--pokemon-yellow);
|
||||||
@@ -175,7 +222,33 @@ svg {
|
|||||||
align-content: start;
|
align-content: start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
scrollbar-color: rgba(104, 116, 135, 0.2) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav:hover,
|
||||||
|
.side-nav:focus-within {
|
||||||
|
scrollbar-color: rgba(104, 116, 135, 0.42) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(104, 116, 135, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav:hover::-webkit-scrollbar-thumb,
|
||||||
|
.side-nav:focus-within::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(104, 116, 135, 0.38);
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-nav__link {
|
.side-nav__link {
|
||||||
@@ -208,6 +281,46 @@ svg {
|
|||||||
box-shadow: 0 2px 0 var(--line-strong);
|
box-shadow: 0 2px 0 var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-nav__group {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav__group-trigger {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav__group-trigger.router-link-active {
|
||||||
|
background: rgba(42, 117, 187, 0.12);
|
||||||
|
color: var(--pokemon-blue-deep);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(42, 117, 187, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav__chevron {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-left: auto;
|
||||||
|
transition: transform 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav__children {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 15px;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-left: 2px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav__link--child {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.side-nav__label {
|
.side-nav__label {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -246,6 +359,12 @@ svg {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-actions__label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.language-menu {
|
.language-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -405,6 +524,110 @@ svg {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 95;
|
||||||
|
max-width: min(230px, calc(100vw - 96px));
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: var(--radius-small);
|
||||||
|
background: #172036;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 850;
|
||||||
|
line-height: 1.25;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tooltip::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 100%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-top: 6px solid transparent;
|
||||||
|
border-right: 6px solid #172036;
|
||||||
|
border-bottom: 6px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 901px) {
|
||||||
|
.app-shell--sidebar-collapsed {
|
||||||
|
grid-template-columns: 72px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .site-sidebar__inner {
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .site-sidebar__header {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .site-sidebar__header .brand-lockup {
|
||||||
|
width: 44px;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .site-sidebar__header .brand-lockup > span,
|
||||||
|
.app-shell--sidebar-collapsed .side-nav__label,
|
||||||
|
.app-shell--sidebar-collapsed .auth-user__name,
|
||||||
|
.app-shell--sidebar-collapsed .auth-actions__label,
|
||||||
|
.app-shell--sidebar-collapsed .language-menu__glyph {
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .side-nav__link,
|
||||||
|
.app-shell--sidebar-collapsed .auth-actions .ui-button,
|
||||||
|
.app-shell--sidebar-collapsed .auth-user,
|
||||||
|
.app-shell--sidebar-collapsed .site-sidebar .language-menu__trigger {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
padding-right: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .side-nav__group-trigger {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .side-nav__chevron {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .side-nav__children {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .side-nav__link--child {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .side-nav__badge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell--sidebar-collapsed .site-sidebar .language-menu__dropdown {
|
||||||
|
bottom: 0;
|
||||||
|
left: calc(100% + 8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
position: relative;
|
position: relative;
|
||||||
--page-padding-x: 24px;
|
--page-padding-x: 24px;
|
||||||
@@ -2990,6 +3213,12 @@ button:disabled,
|
|||||||
.life-reaction-option,
|
.life-reaction-option,
|
||||||
.life-action-tooltip,
|
.life-action-tooltip,
|
||||||
.life-search-control__clear,
|
.life-search-control__clear,
|
||||||
|
.app-shell,
|
||||||
|
.sidebar-collapse-toggle,
|
||||||
|
.sidebar-collapse-toggle__icon,
|
||||||
|
.sidebar-tooltip,
|
||||||
|
.side-nav__link,
|
||||||
|
.side-nav__chevron,
|
||||||
.reorderable-row,
|
.reorderable-row,
|
||||||
.reorderable-list-move,
|
.reorderable-list-move,
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
@@ -6161,6 +6390,10 @@ button:disabled,
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.site-sidebar {
|
.site-sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0 auto 0 0;
|
inset: 0 auto 0 0;
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export const systemWordingMessages = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
|
pokedex: 'Pokedex',
|
||||||
|
habitatDex: 'Habitat Dex',
|
||||||
|
collections: 'Collections',
|
||||||
|
mainGame: 'Main Game',
|
||||||
|
event: 'Event',
|
||||||
pokemon: 'Pokemon',
|
pokemon: 'Pokemon',
|
||||||
eventPokemon: 'Event Pokemon',
|
eventPokemon: 'Event Pokemon',
|
||||||
habitats: 'Habitats',
|
habitats: 'Habitats',
|
||||||
@@ -63,6 +68,8 @@ export const systemWordingMessages = {
|
|||||||
main: 'Main navigation',
|
main: 'Main navigation',
|
||||||
openMenu: 'Open navigation',
|
openMenu: 'Open navigation',
|
||||||
closeMenu: 'Close navigation',
|
closeMenu: 'Close navigation',
|
||||||
|
collapseSidebar: 'Collapse sidebar',
|
||||||
|
expandSidebar: 'Expand sidebar',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
profile: 'Profile',
|
profile: 'Profile',
|
||||||
login: 'Log in',
|
login: 'Log in',
|
||||||
@@ -1288,6 +1295,11 @@ export const systemWordingMessages = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: '首页',
|
home: '首页',
|
||||||
|
pokedex: 'Pokedex',
|
||||||
|
habitatDex: 'Habitat Dex',
|
||||||
|
collections: 'Collections',
|
||||||
|
mainGame: 'Main Game',
|
||||||
|
event: 'Event',
|
||||||
pokemon: 'Pokemon',
|
pokemon: 'Pokemon',
|
||||||
eventPokemon: 'Event Pokemon',
|
eventPokemon: 'Event Pokemon',
|
||||||
habitats: '栖息地',
|
habitats: '栖息地',
|
||||||
@@ -1308,6 +1320,8 @@ export const systemWordingMessages = {
|
|||||||
main: '主导航',
|
main: '主导航',
|
||||||
openMenu: '打开导航',
|
openMenu: '打开导航',
|
||||||
closeMenu: '关闭导航',
|
closeMenu: '关闭导航',
|
||||||
|
collapseSidebar: '收起侧边栏',
|
||||||
|
expandSidebar: '展开侧边栏',
|
||||||
language: '语言',
|
language: '语言',
|
||||||
profile: '个人资料',
|
profile: '个人资料',
|
||||||
login: '登录',
|
login: '登录',
|
||||||
|
|||||||
Reference in New Issue
Block a user