Files
KArtSell.Aegis/frontend/src/shared/shell/KsTabs.vue
T

340 lines
7.3 KiB
Vue

<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useLayoutStore } from './layoutStore'
const router = useRouter()
const layoutStore = useLayoutStore()
const tabs = computed(() => layoutStore.openedTabs)
const activeTabId = computed(() => layoutStore.activeTabId)
const handleTabClick = (path: string) => {
router.push(path)
}
const handleCloseTab = (event: Event, tabId: string) => {
event.stopPropagation()
const wasActive = activeTabId.value === tabId
layoutStore.closeTab(tabId)
if (wasActive) {
const nextActiveTab = tabs.value.find(t => t.id === layoutStore.activeTabId)
if (nextActiveTab) {
router.push(nextActiveTab.path)
} else {
router.push('/home')
}
}
}
const showMenu = ref(false)
const toggleMenu = (event: Event) => {
event.stopPropagation()
showMenu.value = !showMenu.value
}
const closeMenuOnOutsideClick = () => {
if (showMenu.value) {
showMenu.value = false
}
}
onMounted(() => {
window.addEventListener('click', closeMenuOnOutsideClick)
})
onUnmounted(() => {
window.removeEventListener('click', closeMenuOnOutsideClick)
})
const handleCloseOther = (tabId: string) => {
layoutStore.closeOtherTabs(tabId)
showMenu.value = false
}
const handleCloseAll = () => {
layoutStore.closeAllTabs()
showMenu.value = false
}
const isTabActive = (tabId: string) => activeTabId.value === tabId
</script>
<template>
<nav class="ks-tabs ks-workspace-tabs" aria-label="열린 업무">
<div class="ks-tabs__list">
<div
v-for="tab in tabs"
:key="tab.id"
role="button"
tabindex="0"
:class="['ks-tabs__tab', { 'ks-tabs__tab--active': isTabActive(tab.id) }]"
@click="handleTabClick(tab.path)"
@keydown.enter="handleTabClick(tab.path)"
@keydown.space.prevent="handleTabClick(tab.path)"
>
<span class="ks-tabs__icon">{{ tab.icon }}</span>
<span class="ks-tabs__title" :title="tab.title">{{ tab.title }}</span>
<button
type="button"
class="ks-tabs__close"
:aria-label="`${tab.title} 닫기`"
@click="handleCloseTab($event, tab.id)"
>
</button>
</div>
</div>
<!-- Compact Tab Actions Dropdown -->
<div v-if="tabs.length > 0" class="ks-tabs__menu-wrapper">
<button
type="button"
class="ks-tabs__menu-trigger"
title="탭 작업 옵션"
aria-label=" 작업 옵션"
@click="toggleMenu"
>
</button>
<!-- Dropdown Menu -->
<div v-if="showMenu" class="ks-tabs__dropdown">
<button
v-if="tabs.length > 1 && activeTabId"
type="button"
class="ks-tabs__dropdown-item"
@click="handleCloseOther(activeTabId!)"
>
📌 <span>다른 닫기</span>
</button>
<button
type="button"
class="ks-tabs__dropdown-item ks-tabs__dropdown-item--danger"
@click="handleCloseAll"
>
🗑️ <span>전체 닫기</span>
</button>
</div>
</div>
</nav>
</template>
<style scoped>
.ks-tabs {
display: flex;
align-items: center;
height: var(--ks-shell-tabs-height);
background-color: var(--ks-color-surface);
border-bottom: 1px solid var(--ks-color-border);
position: relative;
overflow: visible;
gap: var(--ks-space-2);
padding: 0 var(--ks-space-4);
z-index: 50;
}
.ks-tabs__list {
display: flex;
gap: var(--ks-space-2);
flex: 1;
min-width: 0;
overflow-x: auto;
scroll-behavior: smooth;
align-items: center;
height: 100%;
}
.ks-tabs__tab {
display: flex;
align-items: center;
gap: var(--ks-space-1);
padding: 0 var(--ks-space-2);
height: 26px;
width: 150px;
min-width: 150px;
max-width: 150px;
background-color: var(--ks-color-neutral-100);
border: 1px solid var(--ks-color-border);
border-radius: var(--ks-radius-sm);
color: var(--ks-color-text-muted);
cursor: pointer;
font-size: var(--ks-font-grid);
white-space: nowrap;
flex-shrink: 0;
box-sizing: border-box;
transition: var(--ks-transition-fast);
}
.ks-tabs__tab:hover {
background-color: var(--ks-color-neutral-200);
color: var(--ks-color-text);
}
.ks-tabs__tab--active {
background-color: var(--ks-color-surface);
color: var(--ks-color-action);
border-color: var(--ks-color-action);
box-shadow: var(--ks-shadow-sm);
font-weight: 600;
}
.ks-tabs__icon {
font-size: 16px;
flex-shrink: 0;
}
.ks-tabs__title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ks-tabs__close {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0 var(--spacing-1);
opacity: 0.6;
transition: opacity var(--transition-base);
flex-shrink: 0;
font-size: 14px;
}
.ks-tabs__close:hover {
opacity: 1;
}
.ks-tabs__menu-wrapper {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
border-left: 1px solid var(--ks-color-border);
padding-left: var(--ks-space-1);
margin-left: var(--ks-space-1);
}
.ks-tabs__menu-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: transparent;
border: 1px solid transparent;
border-radius: var(--ks-radius-sm);
color: var(--ks-color-text-muted);
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: var(--ks-transition-fast);
}
.ks-tabs__menu-trigger:hover {
background-color: var(--ks-color-neutral-100);
color: var(--ks-color-text);
border-color: var(--ks-color-border);
}
.ks-tabs__dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: var(--ks-color-surface);
border: 1px solid var(--ks-color-border);
border-radius: var(--ks-radius-sm);
box-shadow: var(--ks-shadow-md);
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
z-index: 120;
min-width: 120px;
}
.ks-tabs__dropdown-item {
display: flex;
align-items: center;
gap: var(--ks-space-2);
width: 100%;
padding: 5px 8px;
background: transparent;
border: none;
border-radius: var(--ks-radius-sm);
color: var(--ks-color-text);
font-size: var(--ks-font-caption);
cursor: pointer;
text-align: left;
white-space: nowrap;
transition: var(--ks-transition-fast);
}
.ks-tabs__dropdown-item:hover {
background-color: var(--ks-color-neutral-100);
}
.ks-tabs__dropdown-item--danger {
color: var(--ks-color-danger);
}
.ks-tabs__dropdown-item--danger:hover {
background-color: rgba(220, 38, 38, 0.08);
}
/* Scrollbar styling */
.ks-tabs__list::-webkit-scrollbar {
height: 4px;
}
.ks-tabs__list::-webkit-scrollbar-track {
background: transparent;
}
.ks-tabs__list::-webkit-scrollbar-thumb {
background: var(--color-border-secondary);
border-radius: 2px;
}
.ks-tabs__list::-webkit-scrollbar-thumb:hover {
background: var(--color-border-primary);
}
/* Mobile */
@media (max-width: 768px) {
.ks-tabs {
height: 40px;
padding: 0 var(--spacing-2);
gap: 0;
}
.ks-tabs__tab {
padding: var(--spacing-1) var(--spacing-2);
}
.ks-tabs__title {
max-width: 100px;
}
.ks-tabs__actions {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ks-tabs,
.ks-tabs__list,
.ks-tabs__tab,
.ks-tabs__action {
transition: none;
scroll-behavior: auto;
}
}
</style>