51 lines
8.9 KiB
Vue
51 lines
8.9 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import type { KbxNavigationResolvedEntry, KbxNavigationSection, KbxRecentNavigation } from '@kbx/contracts'
|
||
|
||
const props=withDefaults(defineProps<{sections:KbxNavigationSection[];favorites?:KbxNavigationResolvedEntry[];recents?:KbxRecentNavigation[];activeScreenId?:string|null;collapsed?:boolean;homeActive?:boolean;preferredModule?:string}>(),{favorites:()=>[],recents:()=>[],activeScreenId:null,collapsed:false,homeActive:false,preferredModule:''})
|
||
const emit=defineEmits<{home:[];open:[entry:KbxNavigationResolvedEntry];openRecent:[entry:KbxRecentNavigation];toggleFavorite:[screenId:string];toggleCollapsed:[];menuSearch:[];moduleSelected:[module:string]}>()
|
||
const moduleLabel:Record<string,string>={OMS:'OMS',ERP:'ERP',WMS:'WMS',COMMON:'공통'}
|
||
const moduleOrder=['OMS','ERP','WMS','COMMON'] as const
|
||
const grouped=computed(()=>moduleOrder.map(module=>({module,label:moduleLabel[module],sections:props.sections.filter(section=>section.module===module)})).filter(group=>group.sections.length))
|
||
const activeModule=computed(()=>props.sections.find(section=>section.entries.some(entry=>entry.screenId===props.activeScreenId))?.module)
|
||
const selectedModule=ref<string>('')
|
||
watch([()=>props.preferredModule,activeModule,grouped],()=>{if(props.preferredModule&&grouped.value.some(x=>x.module===props.preferredModule))selectedModule.value=props.preferredModule;else if(activeModule.value)selectedModule.value=activeModule.value;else if(!grouped.value.some(x=>x.module===selectedModule.value))selectedModule.value=grouped.value[0]?.module??''},{immediate:true})
|
||
const selectedGroup=computed(()=>grouped.value.find(x=>x.module===selectedModule.value)??grouped.value[0])
|
||
const isFavorite=(id:string)=>props.favorites.some(x=>x.screenId===id)
|
||
function chooseModule(module:string){selectedModule.value=module;emit('moduleSelected',module);if(props.collapsed)emit('toggleCollapsed')}
|
||
</script>
|
||
|
||
<template>
|
||
<aside class="side-nav" :class="{collapsed:props.collapsed}" aria-label="업무 메뉴">
|
||
<div class="side-nav__top">
|
||
<button class="home" type="button" :class="{active:homeActive}" :aria-current="homeActive?'page':undefined" @click="emit('home')"><span aria-hidden="true">⌂</span><span v-if="!collapsed">홈</span></button>
|
||
<button class="collapse" type="button" @click="emit('toggleCollapsed')" :aria-label="props.collapsed?'메뉴 펼치기':'메뉴 접기'">{{props.collapsed?'›':'‹'}}</button>
|
||
</div>
|
||
|
||
<template v-if="!props.collapsed">
|
||
<button class="side-nav__search" type="button" aria-keyshortcuts="Control+K" @click="emit('menuSearch')"><span>메뉴·화면 검색</span><kbd>Ctrl K</kbd></button>
|
||
<nav class="module-switcher" aria-label="업무 모듈"><button v-for="group in grouped" :key="group.module" type="button" :aria-pressed="selectedModule===group.module" @click="chooseModule(group.module)">{{group.label}}</button></nav>
|
||
|
||
<section v-if="props.favorites.length" class="personal"><h2>즐겨찾기</h2><button v-for="entry in props.favorites.slice(0,6)" :key="entry.screenId" class="nav-item favorite" :class="{active:entry.screenId===props.activeScreenId}" @click="emit('open',entry)"><span aria-hidden="true">★</span><span>{{entry.title}}</span></button></section>
|
||
<section v-if="props.recents.length" class="personal"><h2>최근메뉴</h2><button v-for="entry in props.recents.slice(0,4)" :key="entry.screenId+entry.path" class="nav-item" @click="emit('openRecent',entry)"><span aria-hidden="true">↺</span><span>{{entry.title}}</span></button></section>
|
||
|
||
<section v-if="selectedGroup" class="module-block active">
|
||
<h2 class="module-title">{{selectedGroup.label}} 업무</h2>
|
||
<div v-for="section in selectedGroup.sections" :key="section.key" class="section-block">
|
||
<h3>{{section.label.replace(`${selectedGroup.label} · `,'')}}</h3>
|
||
<div v-for="entry in section.entries" :key="entry.screenId" class="row">
|
||
<button class="nav-item main" :class="{active:entry.screenId===props.activeScreenId}" :aria-current="entry.screenId===props.activeScreenId?'page':undefined" @click="emit('open',entry)"><span>{{entry.title}}</span></button>
|
||
<button v-if="entry.favoriteAllowed!==false" class="star" type="button" :aria-label="isFavorite(entry.screenId)?'즐겨찾기 해제':'즐겨찾기 추가'" :aria-pressed="isFavorite(entry.screenId)" @click="emit('toggleFavorite',entry.screenId)">{{isFavorite(entry.screenId)?'★':'☆'}}</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<div v-else class="collapsed-actions"><button v-for="group in grouped" :key="group.module" type="button" :class="{active:activeModule===group.module}" :title="`${group.label} 메뉴 열기`" @click="chooseModule(group.module)">{{group.module==='COMMON'?'공':group.module.charAt(0)}}</button><button type="button" title="메뉴 검색" aria-label="메뉴 검색" aria-keyshortcuts="Control+K" @click="emit('menuSearch')">⌕</button></div>
|
||
</aside>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.side-nav{position:relative;width:var(--kbx-side-nav-width);min-width:var(--kbx-side-nav-width);border-right:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface);overflow:auto;padding:var(--kbx-space-2) var(--kbx-space-2) var(--kbx-space-5)}.side-nav.collapsed{width:var(--kbx-side-nav-collapsed-width);min-width:var(--kbx-side-nav-collapsed-width);overflow:hidden}.side-nav__top{position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:var(--kbx-space-1);padding-bottom:var(--kbx-space-2);background:var(--kbx-color-surface)}.home{height:var(--kbx-control-sm);flex:1;display:flex;align-items:center;gap:var(--kbx-space-2);border:0;border-radius:var(--kbx-radius-sm);background:transparent;padding:0 var(--kbx-space-2);text-align:left}.home:hover,.home.active{background:var(--kbx-color-surface-hover);color:var(--kbx-color-primary)}.collapse{width:var(--kbx-control-xs);height:var(--kbx-control-xs);border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface);border-radius:var(--kbx-radius-sm)}.side-nav__search{width:100%;height:var(--kbx-control-height);display:flex;align-items:center;justify-content:space-between;padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface-muted);color:var(--kbx-color-text-muted)}.side-nav__search kbd{font-size:var(--kbx-font-xs)}.module-switcher{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--kbx-space-1);margin-top:var(--kbx-space-2)}.module-switcher button{height:var(--kbx-control-xs);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);font-size:var(--kbx-font-xs)}.module-switcher button[aria-pressed="true"]{border-color:var(--kbx-color-primary);color:var(--kbx-color-primary);font-weight:600;background:var(--kbx-color-surface-hover)}.side-nav h2{margin:var(--kbx-space-3) var(--kbx-space-2) var(--kbx-space-1);font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted)}.module-block{margin-top:var(--kbx-space-2);padding-top:var(--kbx-space-1);border-top:var(--kbx-border-width) solid var(--kbx-color-border)}.module-block.active>.module-title{color:var(--kbx-color-primary)}.section-block h3{margin:var(--kbx-space-2) var(--kbx-space-2) var(--kbx-space-1);font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted)}.row{display:flex;align-items:center}.nav-item{min-height:var(--kbx-control-sm);border:0;background:transparent;border-radius:var(--kbx-radius-sm);display:flex;align-items:center;gap:var(--kbx-space-2);width:100%;padding:0 var(--kbx-space-2);text-align:left;color:var(--kbx-color-text)}.nav-item:hover,.nav-item.active{background:var(--kbx-color-surface-hover)}.nav-item.active{color:var(--kbx-color-primary);font-weight:600}.nav-item.main{min-width:0;flex:1}.nav-item.main span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.star{width:var(--kbx-control-xs);height:var(--kbx-control-xs);border:0;background:transparent;color:var(--kbx-color-text-muted);border-radius:var(--kbx-radius-sm)}.star:hover{background:var(--kbx-color-surface-hover);color:var(--kbx-color-primary)}.collapsed .side-nav__top{flex-direction:column}.collapsed .home{width:var(--kbx-control-md);flex:none;justify-content:center;padding:0}.collapsed .collapse{width:var(--kbx-control-md)}.collapsed-actions{display:flex;flex-direction:column;align-items:center;gap:var(--kbx-space-2);padding-top:var(--kbx-space-2);border-top:var(--kbx-border-width) solid var(--kbx-color-border)}.collapsed-actions button{width:var(--kbx-control-md);height:var(--kbx-control-md);border:0;border-radius:var(--kbx-radius-sm);background:transparent;font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted)}.collapsed-actions button:hover,.collapsed-actions button.active{background:var(--kbx-color-surface-hover);color:var(--kbx-color-primary)}
|
||
</style>
|