V13-FE-006: consolidate approved UI and contract hardening
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s

This commit is contained in:
2026-08-13 02:41:00 +09:00
parent d79edae546
commit 3f293d8aa8
1278 changed files with 14384 additions and 1664 deletions
+23 -3
View File
@@ -16,6 +16,8 @@ const router = useRouter()
const preference = useScreenPreferenceStore()
const workspace = useWorkspaceStore()
const collapsed = ref(false)
const mobileNavOpen = ref(false)
const globalHeader = ref<InstanceType<typeof KsGlobalHeader> | null>(null)
const menuSearchOpen = ref(false)
const appVersion = import.meta.env.VITE_APP_VERSION ?? '0.1.0'
@@ -40,6 +42,10 @@ watch(() => route.fullPath, () => {
function goHome() {
router.push('/home')
}
function closeMobileNavigation() {
mobileNavOpen.value = false
requestAnimationFrame(() => globalHeader.value?.focusMenuButton())
}
function openMenuSearch() {
menuSearchOpen.value = true
}
@@ -60,6 +66,10 @@ function closeTab(tab: WorkspaceTab) {
}
function onKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && mobileNavOpen.value) {
closeMobileNavigation()
return
}
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
event.preventDefault()
menuSearchOpen.value = true
@@ -72,11 +82,17 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
<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" />
<KsGlobalHeader ref="globalHeader" :product-name="productName" :environment="environment" :automation-status="automationStatus" @home="goHome" @menu-search="openMenuSearch" @menu-toggle="mobileNavOpen = true" />
<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" />
<KsSideNavigation :sections="sections()" :favorites="favorites()" :active-path="route.path" :collapsed="collapsed" :collapsed-sections="preference.collapsedSectionModules" :mobile-open="mobileNavOpen" @toggle-collapsed="collapsed = !collapsed" @toggle-section="preference.toggleSection" @close-mobile="closeMobileNavigation" />
<button v-if="mobileNavOpen" type="button" class="ks-mobile-backdrop" aria-label="메뉴 닫기" @click="closeMobileNavigation" />
<main id="ks-main" class="ks-app-shell__main" tabindex="-1">
<nav class="ks-breadcrumb" aria-label="현재 위치">
<RouterLink to="/home"></RouterLink>
<span aria-hidden="true"></span>
<span aria-current="page">{{ typeof route.meta.title === 'string' ? route.meta.title : route.path }}</span>
</nav>
<slot />
</main>
</div>
@@ -90,9 +106,13 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
.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-breadcrumb { display: flex; gap: var(--ks-space-2); align-items: center; margin-bottom: var(--ks-space-4); color: var(--ks-color-text-muted); font-size: var(--ks-font-caption); }
.ks-breadcrumb a { color: inherit; text-decoration: none; }
.ks-breadcrumb a:hover, .ks-breadcrumb a:focus-visible { color: var(--ks-color-text); text-decoration: underline; }
.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; } }
.ks-mobile-backdrop { display: none; }
@media (max-width: 900px) { .ks-app-shell__body { flex-direction: column; } .ks-mobile-backdrop { display: block; position: fixed; inset: 0; z-index: 40; border: 0; background: rgb(15 23 42 / 45%); } }
</style>