V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { buildNavigationEntries, groupByModule } from './navigationCatalog'
|
||||
import { useScreenPreferenceStore } from './screenPreferenceStore'
|
||||
import { useWorkspaceStore, type WorkspaceTab } from './workspaceStore'
|
||||
import KsGlobalHeader from './KsGlobalHeader.vue'
|
||||
import KsSideNavigation from './KsSideNavigation.vue'
|
||||
import KsWorkspaceTabs from './KsWorkspaceTabs.vue'
|
||||
import KsMenuSearch from './KsMenuSearch.vue'
|
||||
import type { NavigationEntry } from './navigationCatalog'
|
||||
|
||||
defineProps<{ productName?: string; environment?: string; automationStatus?: string }>()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const preference = useScreenPreferenceStore()
|
||||
const workspace = useWorkspaceStore()
|
||||
const collapsed = ref(false)
|
||||
const menuSearchOpen = ref(false)
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION ?? '0.1.0'
|
||||
|
||||
const entries = () => buildNavigationEntries(router.getRoutes()).filter(entry => !entry.internalOnly)
|
||||
const sections = () => groupByModule(entries())
|
||||
const favorites = () => preference.favoriteScreenIds
|
||||
.map(id => entries().find(entry => entry.screenId === id))
|
||||
.filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))
|
||||
const activeTabKey = () => {
|
||||
const screenId = route.meta.screenId
|
||||
return typeof screenId === 'string' ? `${screenId}:${route.path}` : null
|
||||
}
|
||||
|
||||
watch(() => route.fullPath, () => {
|
||||
const meta = route.meta
|
||||
if (typeof meta.screenId !== 'string' || meta.module === 'Home') return
|
||||
const title = typeof meta.title === 'string' ? meta.title : route.path
|
||||
preference.recordVisit(meta.screenId, route.path)
|
||||
workspace.open(meta.screenId, route.path, title)
|
||||
}, { immediate: true })
|
||||
|
||||
function goHome() {
|
||||
router.push('/home')
|
||||
}
|
||||
function openMenuSearch() {
|
||||
menuSearchOpen.value = true
|
||||
}
|
||||
function selectEntry(entry: NavigationEntry) {
|
||||
menuSearchOpen.value = false
|
||||
router.push(entry.path)
|
||||
}
|
||||
function selectTab(tab: WorkspaceTab) {
|
||||
router.push(tab.path)
|
||||
}
|
||||
function closeTab(tab: WorkspaceTab) {
|
||||
const wasActive = activeTabKey() === `${tab.screenId}:${tab.path}`
|
||||
workspace.close(tab)
|
||||
if (wasActive) {
|
||||
const next = workspace.tabs.at(-1)
|
||||
router.push(next ? next.path : '/home')
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
|
||||
event.preventDefault()
|
||||
menuSearchOpen.value = true
|
||||
}
|
||||
}
|
||||
onMounted(() => window.addEventListener('keydown', onKeydown))
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ks-app-shell">
|
||||
<a class="ks-skip" href="#ks-main">본문으로 건너뛰기</a>
|
||||
<KsGlobalHeader :product-name="productName" :environment="environment" :automation-status="automationStatus" @home="goHome" @menu-search="openMenuSearch" />
|
||||
<KsWorkspaceTabs :tabs="workspace.visibleTabs" :overflow-count="workspace.overflowCount" :active-key="activeTabKey()" @select="selectTab" @close="closeTab" @toggle-pin="workspace.togglePin" />
|
||||
<div class="ks-app-shell__body">
|
||||
<KsSideNavigation :sections="sections()" :favorites="favorites()" :active-path="route.path" :collapsed="collapsed" @toggle-collapsed="collapsed = !collapsed" />
|
||||
<main id="ks-main" class="ks-app-shell__main" tabindex="-1">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<footer class="ks-app-shell__footer">RESEARCH_CANDIDATE_NOT_PRODUCTION</footer>
|
||||
<div class="ks-app-shell__version" data-testid="app-version" aria-label="애플리케이션 버전">v{{ appVersion }} · UI contract 4.0</div>
|
||||
<KsMenuSearch :open="menuSearchOpen" :entries="entries()" @close="menuSearchOpen = false" @select="selectEntry" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-app-shell { min-height: 100vh; display: grid; grid-template-rows: auto auto 1fr auto; background: var(--ks-color-canvas); }
|
||||
.ks-app-shell__body { display: flex; min-height: 0; }
|
||||
.ks-app-shell__main { flex: 1; min-width: 0; padding: var(--ks-space-6); overflow: auto; }
|
||||
.ks-app-shell__footer { padding: var(--ks-space-2) var(--ks-space-6); border-top: 1px solid var(--ks-color-border); background: var(--ks-color-surface); color: var(--ks-color-text-muted); font-size: var(--ks-font-caption); }
|
||||
.ks-app-shell__version { position: fixed; right: var(--ks-space-3); bottom: var(--ks-space-2); z-index: 20; padding: .25rem .5rem; border: 1px solid var(--ks-color-border); border-radius: var(--ks-radius-sm); background: rgb(255 255 255 / 92%); color: var(--ks-color-text-muted); font-size: .7rem; box-shadow: var(--ks-shadow-sm); }
|
||||
.ks-skip { position: fixed; left: var(--ks-space-2); top: -4rem; z-index: 1000; padding: var(--ks-space-2); background: var(--ks-color-surface); }
|
||||
.ks-skip:focus { top: var(--ks-space-2); }
|
||||
@media (max-width: 900px) { .ks-app-shell__body { flex-direction: column; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user