c41e5063b7
Removed entire kbx-foundation-v36 directory as it's been replaced by the new KBX Foundation v4 patterns implemented in this session: - Registry-driven screen definitions - Density-aware UI adapter components - Feature module templates (ShadowRun, Models) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
73 lines
4.2 KiB
Vue
73 lines
4.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { KbxProblem } from '@kbx/contracts'
|
|
import KbxButton from './KbxButton.vue'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
problem: KbxProblem
|
|
allowedActionIds?: readonly string[]
|
|
dismissible?: boolean
|
|
}>(), {
|
|
allowedActionIds: () => [],
|
|
dismissible: true,
|
|
})
|
|
|
|
const emit = defineEmits<{ action:[string]; retry:[]; dismiss:[] }>()
|
|
|
|
const kindLabel: Record<KbxProblem['type'], string> = {
|
|
validation: '입력 오류',
|
|
'business-rule': '업무 규칙',
|
|
conflict: '동시 수정',
|
|
permission: '권한',
|
|
'not-found': '데이터',
|
|
integration: '외부 연계',
|
|
system: '시스템',
|
|
}
|
|
|
|
const tone = computed(() => {
|
|
if (props.problem.type === 'permission' || props.problem.type === 'system') return 'danger'
|
|
if (props.problem.type === 'integration') return props.problem.retryable ? 'warning' : 'danger'
|
|
if (props.problem.type === 'business-rule' || props.problem.type === 'conflict') return 'warning'
|
|
return 'info'
|
|
})
|
|
const retryable = computed(() => (props.problem.type === 'integration' || props.problem.type === 'system') && props.problem.retryable === true)
|
|
const allowed = computed(() => new Set(props.allowedActionIds))
|
|
const actions = computed(() => props.problem.type === 'business-rule'
|
|
? (props.problem.actions ?? []).filter(action => allowed.value.has(action.id))
|
|
: [])
|
|
</script>
|
|
|
|
<template>
|
|
<section class="kbx-problem-feedback" :data-tone="tone" :data-problem-type="problem.type" role="alert" aria-live="assertive">
|
|
<div class="kbx-problem-feedback__copy">
|
|
<div class="kbx-problem-feedback__heading">
|
|
<span class="kbx-problem-feedback__kind">{{ kindLabel[problem.type] }}</span>
|
|
<strong>{{ problem.title }}</strong>
|
|
</div>
|
|
<p v-if="problem.detail">{{ problem.detail }}</p>
|
|
<ul v-if="problem.type === 'validation' && problem.errors.length" class="kbx-problem-feedback__validation">
|
|
<li v-for="error in problem.errors.slice(0,3)" :key="`${error.rowKey ?? ''}:${error.field ?? ''}:${error.code}`">{{ error.message }}</li>
|
|
<li v-if="problem.errors.length > 3">외 {{ problem.errors.length - 3 }}건의 입력 오류가 있습니다.</li>
|
|
</ul>
|
|
<small v-if="problem.correlationId">참조번호 {{ problem.correlationId }}</small>
|
|
</div>
|
|
<div class="kbx-problem-feedback__actions">
|
|
<KbxButton v-for="action in actions" :key="action.id" :label="action.label" variant="secondary" @click="emit('action', action.id)" />
|
|
<KbxButton v-if="retryable" label="다시 시도" variant="secondary" @click="emit('retry')" />
|
|
<KbxButton v-if="dismissible" label="닫기" variant="ghost" @click="emit('dismiss')" />
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.kbx-problem-feedback{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--kbx-space-3);padding:var(--kbx-space-3);border:var(--kbx-border-width) solid var(--kbx-color-info-border);border-left:var(--kbx-accent-border-width) solid var(--kbx-color-primary);background:var(--kbx-color-info-surface)}
|
|
.kbx-problem-feedback[data-tone="warning"]{border-color:var(--kbx-color-warning-border);border-left-color:var(--kbx-color-warning);background:var(--kbx-color-warning-surface)}
|
|
.kbx-problem-feedback[data-tone="danger"]{border-color:var(--kbx-color-danger-border);border-left-color:var(--kbx-color-danger);background:var(--kbx-color-danger-surface)}
|
|
.kbx-problem-feedback__copy{min-width:0;display:grid;gap:var(--kbx-space-1)}
|
|
.kbx-problem-feedback__heading{display:flex;align-items:center;flex-wrap:wrap;gap:var(--kbx-space-2)}
|
|
.kbx-problem-feedback__kind{font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted)}
|
|
.kbx-problem-feedback p,.kbx-problem-feedback small{margin:0;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-sm);line-height:1.5}.kbx-problem-feedback__validation{margin:var(--kbx-space-1) 0 0;padding-left:var(--kbx-space-5);color:var(--kbx-color-text);font-size:var(--kbx-font-sm)}
|
|
.kbx-problem-feedback__actions{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:var(--kbx-space-1)}
|
|
@media(max-width:48rem){.kbx-problem-feedback{flex-direction:column}.kbx-problem-feedback__actions{width:100%;justify-content:flex-start}}
|
|
</style>
|