<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import type { KbxNavigationResolvedEntry } from '@kbx/contracts'
import { searchNavigationEntries } from './navigationSearch'
const props=withDefaults(defineProps<{open:boolean;entries:KbxNavigationResolvedEntry[];priorityScreenIds?:string[]}>(),{priorityScreenIds:()=>[]})
const emit=defineEmits<{close:[];select:[entry:KbxNavigationResolvedEntry]}>()
const query=ref('');const selected=ref(0);const input=ref<HTMLInputElement|null>(null);const dialog=ref<HTMLElement|null>(null);let restoreFocus:HTMLElement|null=null
const results=computed(()=>searchNavigationEntries(props.entries,query.value,12,props.priorityScreenIds));const byId=computed(()=>new Map(props.entries.map(x=>[x.screenId,x])))
const activeOptionId=computed(()=>results.value[selected.value]?`kbx-menu-result-${results.value[selected.value].screenId}`:undefined)
function choose(index=selected.value){const result=results.value[index];if(!result)return;const entry=byId.value.get(result.screenId);if(entry){emit('select',entry);emit('close')}}
function trapTab(e:KeyboardEvent){const root=dialog.value;if(!root)return;const nodes=[...root.querySelectorAll<HTMLElement>('input,button,[tabindex]:not([tabindex="-1"])')].filter(node=>!node.hasAttribute('disabled'));if(!nodes.length)return;const first=nodes[0],last=nodes.at(-1);if(e.shiftKey&&document.activeElement===first){e.preventDefault();last?.focus()}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first?.focus()}}
function onKeydown(e:KeyboardEvent){if(e.key==='Escape'){e.preventDefault();emit('close')}else if(e.key==='ArrowDown'){e.preventDefault();selected.value=Math.min(selected.value+1,results.value.length-1)}else if(e.key==='ArrowUp'){e.preventDefault();selected.value=Math.max(0,selected.value-1)}else if(e.key==='Enter'){e.preventDefault();choose()}else if(e.key==='Tab'){trapTab(e)}}
watch(()=>props.open,async (value,previous)=>{if(value){restoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;query.value='';selected.value=0;await nextTick();input.value?.focus()}else if(previous){await nextTick();restoreFocus?.focus();restoreFocus=null}});watch(results,()=>{selected.value=0})
</script>
<template><div v-if="props.open" class="overlay" @mousedown.self="emit('close')"><section ref="dialog" class="dialog" role="dialog" aria-modal="true" aria-labelledby="kbx-menu-search-title" @keydown="onKeydown"><h2 id="kbx-menu-search-title" class="sr-only">메뉴·화면 검색</h2><input ref="input" v-model="query" type="search" placeholder="메뉴명, 화면코드, 업무명 검색" autocomplete="off" role="combobox" aria-autocomplete="list" aria-controls="kbx-menu-results" :aria-expanded="results.length>0" :aria-activedescendant="activeOptionId"/><div class="hint"><span v-if="!query">즐겨찾기·최근업무를 우선 표시합니다.</span><span v-else role="status" aria-live="polite">검색 결과 {{results.length}}건</span><span>↑↓ 이동 · Enter 열기 · Esc 닫기</span></div><div id="kbx-menu-results" role="listbox" aria-label="검색 결과"><button v-for="(result,index) in results" :id="`kbx-menu-result-${result.screenId}`" :key="result.screenId" type="button" role="option" :aria-selected="index===selected" class="result" :class="{selected:index===selected}" @mouseenter="selected=index" @click="choose(index)"><span class="main"><b>{{result.title}}</b><small>{{result.module}} · {{result.section}}</small></span><code>{{result.screenId}}</code></button></div><p v-if="!results.length" class="empty" role="status">일치하는 메뉴가 없습니다.</p></section></div></template>
<style scoped>.overlay{position:fixed;inset:0;z-index:200;background:var(--kbx-color-overlay);display:flex;align-items:flex-start;justify-content:center;padding-top:var(--kbx-menu-search-top)}.dialog{width:min(var(--kbx-menu-search-width),calc(100vw - var(--kbx-space-6)));max-height:70vh;overflow:auto;background:var(--kbx-color-surface);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-md);box-shadow:var(--kbx-shadow-overlay);padding:var(--kbx-space-2)}.dialog input{width:100%;height:var(--kbx-control-lg);border:var(--kbx-border-width) solid var(--kbx-color-border-strong);border-radius:var(--kbx-radius-sm);padding:0 var(--kbx-space-3);font-size:var(--kbx-font-md)}.hint{display:flex;justify-content:space-between;gap:var(--kbx-space-2);padding:var(--kbx-space-2) var(--kbx-space-1);color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.result{width:100%;min-height:var(--kbx-menu-search-result-height);display:flex;align-items:center;justify-content:space-between;text-align:left;border:0;border-radius:var(--kbx-radius-sm);background:transparent;padding:var(--kbx-space-1) var(--kbx-space-2)}.result.selected{background:var(--kbx-color-surface-hover)}.main{display:flex;flex-direction:column;gap:var(--kbx-space-1)}.main small,.result code{color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.empty{text-align:center;color:var(--kbx-color-text-muted);padding:var(--kbx-space-6)}.sr-only{position:absolute;width:var(--kbx-visually-hidden-size);height:var(--kbx-visually-hidden-size);padding:0;margin:calc(var(--kbx-visually-hidden-size) * -1);overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}</style>
