V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { KbxScreenDefinition, KbxScreenTemplateCode, KbxScreenType } from '@kbx/contracts'
|
||||
import KbxCommandBar from './KbxCommandBar.vue'
|
||||
import KbxDataState from './KbxDataState.vue'
|
||||
import KbxPageHeader from './KbxPageHeader.vue'
|
||||
import { KbxScreenUtilityHostKey, type KbxScreenUtilityTab } from '../utility/host'
|
||||
import { KbxPermissionHostKey } from '../permission/host'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
screen: KbxScreenDefinition
|
||||
/** Template wrapper must state the canonical screen type it implements. */
|
||||
expectedType?: KbxScreenType
|
||||
templateCode?: KbxScreenTemplateCode
|
||||
selectionCount?: number
|
||||
can?: (permission: string) => boolean
|
||||
breadcrumb?: string
|
||||
status?: string
|
||||
dirty?: boolean
|
||||
stickyCommands?: boolean
|
||||
commandBar?: boolean
|
||||
suppressDefaultUtility?: boolean
|
||||
}>(), {
|
||||
expectedType: undefined,
|
||||
templateCode: undefined,
|
||||
selectionCount: 0,
|
||||
breadcrumb: '',
|
||||
status: '',
|
||||
dirty: false,
|
||||
stickyCommands: true,
|
||||
commandBar: true,
|
||||
suppressDefaultUtility: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ command: [string] }>()
|
||||
const utilityHost=inject(KbxScreenUtilityHostKey,null)
|
||||
const permissionHost=inject(KbxPermissionHostKey,null)
|
||||
const defaultUtilities=computed(()=>utilityHost?.available(props.screen)??[])
|
||||
const utilityLabel:Record<KbxScreenUtilityTab,string>={help:'도움말',ai:'AI',suggestion:'제안'}
|
||||
const templateMismatch=computed(()=>Boolean((props.expectedType&&props.screen.type!==props.expectedType)||(props.templateCode&&props.screen.templateCode!==props.templateCode)))
|
||||
function canPermission(permission:string){return props.can?props.can(permission):(permissionHost?.has(permission)??true)}
|
||||
const permissionDenied=computed(()=>Boolean(props.screen.permissions?.some(permission=>!canPermission(permission))))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="kbx-screen-frame"
|
||||
:data-screen-id="screen.id"
|
||||
:data-screen-type="screen.type"
|
||||
:data-template="templateCode"
|
||||
:data-status="status || undefined"
|
||||
:data-dirty="dirty || undefined"
|
||||
:data-access="permissionDenied?'denied':'allowed'"
|
||||
>
|
||||
<KbxPageHeader
|
||||
data-kbx-surface="page-header"
|
||||
:title="screen.title"
|
||||
:description="screen.description"
|
||||
:breadcrumb="breadcrumb"
|
||||
:status="status"
|
||||
:dirty="dirty"
|
||||
>
|
||||
<template #utility><slot name="utility" /><nav v-if="!suppressDefaultUtility && defaultUtilities.length" class="kbx-screen-frame__utility" aria-label="화면 보조기능"><button v-for="tab in defaultUtilities" :key="tab" type="button" @click="utilityHost?.open(screen,tab)">{{utilityLabel[tab]}}</button></nav></template>
|
||||
</KbxPageHeader>
|
||||
|
||||
<KbxDataState v-if="templateMismatch" class="kbx-screen-frame__contract-error" state="error" title="화면 템플릿 구성이 올바르지 않습니다." detail="화면 정의와 표준 화면 유형이 일치하지 않습니다. 안전을 위해 업무 영역을 표시하지 않습니다." />
|
||||
<KbxDataState v-else-if="permissionDenied" class="kbx-screen-frame__contract-error" state="error" title="이 화면을 사용할 권한이 없습니다." detail="현재 권한으로는 이 업무 화면을 열 수 없습니다. 메뉴 검색에서 사용 가능한 업무를 선택하세요." />
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="commandBar && ((screen.commands?.length ?? 0) > 0 || $slots.commands)"
|
||||
class="kbx-screen-frame__commands"
|
||||
data-kbx-surface="command-bar"
|
||||
:class="{ 'is-sticky': stickyCommands }"
|
||||
>
|
||||
<slot name="commands">
|
||||
<KbxCommandBar
|
||||
:commands="screen.commands ?? []"
|
||||
:selection-count="selectionCount"
|
||||
:status="status"
|
||||
:dirty="dirty"
|
||||
:can="canPermission"
|
||||
@command="emit('command', $event)"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.notice" class="kbx-screen-frame__notice"><slot name="notice" /></div>
|
||||
<div class="kbx-screen-frame__body"><slot /></div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kbx-screen-frame {display:flex;flex-direction:column;min-height:0;height:100%;background:var(--kbx-color-surface)}
|
||||
.kbx-screen-frame__commands{min-height:var(--kbx-command-bar-height);padding:0 var(--kbx-screen-inline-padding);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface);z-index:8}
|
||||
.kbx-screen-frame__commands.is-sticky{position:sticky;top:0}.kbx-screen-frame__notice{padding:var(--kbx-space-2) var(--kbx-screen-inline-padding) 0}.kbx-screen-frame__body{min-height:0;flex:1;display:flex;flex-direction:column;gap:var(--kbx-screen-section-gap);padding:var(--kbx-space-2) var(--kbx-screen-inline-padding) var(--kbx-space-3)}
|
||||
.kbx-screen-frame__utility{display:flex;align-items:center;gap:var(--kbx-space-1)}.kbx-screen-frame__utility button{height:var(--kbx-control-xs);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);font-size:var(--kbx-font-xs)}.kbx-screen-frame__utility button:hover{background:var(--kbx-color-surface-hover);border-color:var(--kbx-color-border-strong)}.kbx-screen-frame__contract-error{margin:var(--kbx-space-3) var(--kbx-screen-inline-padding)}
|
||||
</style>
|
||||
Reference in New Issue
Block a user