V13-FE-011: finalize search list layout slice

This commit is contained in:
2026-08-09 02:57:26 +09:00
parent 9efd202e76
commit 6422cb2b13
984 changed files with 120811 additions and 1498 deletions
@@ -0,0 +1,96 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { KbxAiAnswer, KbxAiScreenContext, KbxHelpContent, KbxSuggestionContext, KbxSuggestionRequest } from '@kbx/contracts'
import KbxHelpPanel from './KbxHelpPanel.vue'
import KbxAiAssistant from './KbxAiAssistant.vue'
import KbxUserSuggestionPanel from './KbxUserSuggestionPanel.vue'
type UtilityTab = 'help' | 'ai' | 'suggestion'
const props = defineProps<{
help?: KbxHelpContent
aiContext?: KbxAiScreenContext
aiAnswer?: KbxAiAnswer | null
aiLoading?: boolean
aiError?: string
aiCurrentScreenLabel?: string
aiQuickQuestions?: string[]
suggestionContext?: KbxSuggestionContext
suggestionSubmitting?: boolean
openTab?: UtilityTab | null
triggerless?: boolean
}>()
const emit = defineEmits<{
aiAsk: [question: string]
aiAction: [actionId: string]
aiOpenProposal: []
suggestionSubmit: [request: KbxSuggestionRequest]
close: []
}>()
const open = ref(false)
const active = ref<UtilityTab>('help')
const availableTabs = computed(() => [
props.help && 'help',
props.aiContext && 'ai',
props.suggestionContext && 'suggestion',
].filter(Boolean) as UtilityTab[])
function show(tab: UtilityTab) { active.value = tab; open.value = true }
function close(){ open.value=false; emit('close') }
watch(() => props.openTab, tab => { if(tab)show(tab); else if(props.triggerless)open.value=false }, { immediate:true })
</script>
<template>
<div class="kbx-utility-rail">
<nav v-if="!triggerless" class="kbx-utility-rail__buttons" aria-label="화면 보조기능">
<button v-if="help" type="button" @click="show('help')">도움말</button>
<button v-if="aiContext" type="button" @click="show('ai')">AI</button>
<button v-if="suggestionContext" type="button" @click="show('suggestion')">제안</button>
</nav>
<div v-if="open" class="kbx-utility-rail__backdrop" @click.self="close">
<aside class="kbx-utility-rail__panel" role="dialog" aria-modal="false" aria-label="화면 보조기능">
<header class="kbx-utility-rail__header">
<nav>
<button v-for="tab in availableTabs" :key="tab" type="button" :aria-current="active === tab ? 'page' : undefined" @click="active = tab">
{{ tab === 'help' ? '도움말' : tab === 'ai' ? 'AI' : '제안' }}
</button>
</nav>
<button type="button" aria-label="닫기" @click="close">×</button>
</header>
<KbxHelpPanel v-if="active === 'help' && help" :content="help" />
<KbxAiAssistant
v-else-if="active === 'ai' && aiContext"
:context="aiContext"
:answer="aiAnswer"
:loading="aiLoading"
:error="aiError"
:current-screen-label="aiCurrentScreenLabel"
:quick-questions="aiQuickQuestions"
@ask="emit('aiAsk', $event)"
@action="emit('aiAction', $event)"
@open-proposal="emit('aiOpenProposal')"
/>
<KbxUserSuggestionPanel
v-else-if="active === 'suggestion' && suggestionContext"
:context="suggestionContext"
:submitting="suggestionSubmitting"
@submit="emit('suggestionSubmit', $event)"
/>
</aside>
</div>
</div>
</template>
<style scoped>
.kbx-utility-rail__buttons{display:flex;align-items:center;gap:var(--kbx-space-1)}
.kbx-utility-rail__buttons button,.kbx-utility-rail__header button{min-height:var(--kbx-control-sm);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);color:var(--kbx-color-text)}
.kbx-utility-rail__backdrop{position:fixed;inset:0;z-index:1000;background:var(--kbx-color-overlay);display:flex;justify-content:flex-end}
.kbx-utility-rail__panel{width:min(var(--kbx-utility-panel-width),94vw);height:100%;overflow:auto;background:var(--kbx-color-surface);border-left:var(--kbx-border-width) solid var(--kbx-color-border);box-shadow:var(--kbx-shadow-overlay)}
.kbx-utility-rail__header{position:sticky;top:0;z-index:2;display:flex;justify-content:space-between;gap:var(--kbx-space-2);padding:var(--kbx-space-2);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface)}
.kbx-utility-rail__header nav{display:flex;gap:var(--kbx-space-1)}
.kbx-utility-rail__header [aria-current="page"]{border-color:var(--kbx-color-primary);color:var(--kbx-color-primary);font-weight:600}
</style>