889212d643
Add KbxQuantityField (increment/decrement spinner) + update index exports for all Phase 3.5–4 components (wrapper, form, specialized fields). Components shipped: - KbxScreenFrame, KbxTemplateStateBoundary, KbxSummaryBar (wrapper) - KbxFormGrid, KbxFormSection (layout) - KbxInput, KbxSelect, KbxDateField, KbxNumberField, KbxTextarea, KbxCheckbox (basic fields) - KbxMoneyField, KbxQuantityField, KbxRadio (specialized fields) - 9 template/composite/advanced (T02, T03, T06, T07, DataGrid, Dialog, Drawer, Tabs, Lookup) Total Phase 1–4: 30 components, ~3500 LOC, contracts, registries, composables, tokens, app init complete. Ready for page implementation using KbxScreenFrame wrapper pattern. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
266 lines
5.5 KiB
Vue
266 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* KBX Lookup Component — v60 simplified
|
|
* Search and select from a list of items (used in forms and grids)
|
|
*/
|
|
import { ref, computed } from 'vue'
|
|
|
|
export interface KbxLookupItem {
|
|
id: string | number
|
|
label: string
|
|
code?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
withDefaults(
|
|
defineProps<{
|
|
items: KbxLookupItem[]
|
|
label?: string
|
|
placeholder?: string
|
|
required?: boolean
|
|
loading?: boolean
|
|
error?: string
|
|
modelValue?: string | number
|
|
}>(),
|
|
{
|
|
placeholder: '검색...',
|
|
modelValue: '',
|
|
}
|
|
)
|
|
|
|
defineEmits<{
|
|
'update:modelValue': [id: string | number]
|
|
select: [item: KbxLookupItem]
|
|
}>()
|
|
|
|
const search = ref('')
|
|
const open = ref(false)
|
|
|
|
const props = defineProps<{
|
|
items: KbxLookupItem[]
|
|
label?: string
|
|
placeholder?: string
|
|
required?: boolean
|
|
loading?: boolean
|
|
error?: string
|
|
modelValue?: string | number
|
|
}>()
|
|
|
|
const filtered = computed(() => {
|
|
if (!search.value) return props.items
|
|
const q = search.value.toLowerCase()
|
|
return props.items.filter(
|
|
item => item.label.toLowerCase().includes(q) || item.code?.toLowerCase().includes(q)
|
|
)
|
|
})
|
|
|
|
const selectedItem = computed(() => props.items.find(item => item.id === props.modelValue))
|
|
|
|
const selectItem = (item: KbxLookupItem) => {
|
|
emit('update:modelValue', item.id)
|
|
emit('select', item)
|
|
open.value = false
|
|
search.value = ''
|
|
}
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [id: string | number]
|
|
select: [item: KbxLookupItem]
|
|
}>()
|
|
</script>
|
|
|
|
<template>
|
|
<div class="kbx-lookup-wrapper">
|
|
<label v-if="label" class="kbx-lookup__label">
|
|
{{ label }}
|
|
<span v-if="required" class="kbx-lookup__required">*</span>
|
|
</label>
|
|
<div class="kbx-lookup__input-wrapper">
|
|
<input
|
|
v-model="search"
|
|
:placeholder="selectedItem ? selectedItem.label : placeholder"
|
|
:disabled="loading"
|
|
class="kbx-lookup__input"
|
|
@focus="open = true"
|
|
@input="open = true"
|
|
/>
|
|
<span v-if="selectedItem" class="kbx-lookup__code">{{ selectedItem.code }}</span>
|
|
</div>
|
|
|
|
<div v-if="open" class="kbx-lookup__dropdown">
|
|
<div v-if="loading" class="kbx-lookup__loading">로드 중...</div>
|
|
<div v-else-if="!filtered.length" class="kbx-lookup__empty">
|
|
검색 결과가 없습니다.
|
|
</div>
|
|
<ul v-else class="kbx-lookup__list">
|
|
<li
|
|
v-for="item in filtered"
|
|
:key="item.id"
|
|
:class="['kbx-lookup__item', { 'is-disabled': item.disabled }]"
|
|
@click="!item.disabled && selectItem(item)"
|
|
>
|
|
<span class="kbx-lookup__item-label">{{ item.label }}</span>
|
|
<span v-if="item.code" class="kbx-lookup__item-code">{{ item.code }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div v-if="error" class="kbx-lookup__error">{{ error }}</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.kbx-lookup-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.kbx-lookup__label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--kbx-color-text, #000);
|
|
}
|
|
|
|
.kbx-lookup__required {
|
|
color: var(--kbx-color-danger, #ef4444);
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.kbx-lookup__input-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
border: 1px solid var(--kbx-color-border, #e5e7eb);
|
|
border-radius: 4px;
|
|
background: var(--kbx-color-surface, #fff);
|
|
min-height: 34px;
|
|
}
|
|
|
|
.kbx-lookup__input {
|
|
flex: 1;
|
|
border: none;
|
|
padding: 8px 12px;
|
|
font-size: 14px;
|
|
background: transparent;
|
|
color: var(--kbx-color-text, #000);
|
|
}
|
|
|
|
.kbx-lookup__input:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.kbx-lookup__input-wrapper:focus-within {
|
|
border-color: var(--kbx-color-primary, #3b82f6);
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.kbx-lookup__code {
|
|
padding: 0 8px;
|
|
font-size: 11px;
|
|
color: var(--kbx-color-text-muted, #6b7280);
|
|
border-left: 1px solid var(--kbx-color-border, #e5e7eb);
|
|
}
|
|
|
|
.kbx-lookup__dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--kbx-color-surface, #fff);
|
|
border: 1px solid var(--kbx-color-border, #e5e7eb);
|
|
border-top: none;
|
|
border-radius: 0 0 4px 4px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
z-index: 10;
|
|
margin-top: -1px;
|
|
}
|
|
|
|
.kbx-lookup__loading,
|
|
.kbx-lookup__empty {
|
|
padding: 12px;
|
|
text-align: center;
|
|
font-size: 13px;
|
|
color: var(--kbx-color-text-muted, #6b7280);
|
|
}
|
|
|
|
.kbx-lookup__list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.kbx-lookup__item {
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--kbx-color-border, #e5e7eb);
|
|
font-size: 13px;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.kbx-lookup__item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.kbx-lookup__item:hover:not(.is-disabled) {
|
|
background: var(--kbx-color-border, #e5e7eb);
|
|
}
|
|
|
|
.kbx-lookup__item.is-disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.kbx-lookup__item-label {
|
|
flex: 1;
|
|
}
|
|
|
|
.kbx-lookup__item-code {
|
|
font-size: 11px;
|
|
color: var(--kbx-color-text-muted, #6b7280);
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.kbx-lookup__error {
|
|
font-size: 12px;
|
|
color: var(--kbx-color-danger, #ef4444);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.kbx-lookup__label {
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.kbx-lookup__input-wrapper {
|
|
background: #1f2937;
|
|
border-color: #374151;
|
|
}
|
|
|
|
.kbx-lookup__input {
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.kbx-lookup__code {
|
|
border-left-color: #374151;
|
|
}
|
|
|
|
.kbx-lookup__dropdown {
|
|
background: #1f2937;
|
|
border-color: #374151;
|
|
}
|
|
|
|
.kbx-lookup__item {
|
|
border-bottom-color: #374151;
|
|
}
|
|
|
|
.kbx-lookup__item:hover:not(.is-disabled) {
|
|
background: #374151;
|
|
}
|
|
}
|
|
</style>
|