Files
kjh2064 3f293d8aa8
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s
V13-FE-006: consolidate approved UI and contract hardening
2026-08-13 02:41:00 +09:00

16 lines
3.2 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import type { KbxFieldState } from '@kbx/contracts'
export interface KbxSelectOption { value:string; label:string; disabled?:boolean }
const props=withDefaults(defineProps<{modelValue?:string|null;label:string;options:KbxSelectOption[];required?:boolean;readonly?:boolean;disabled?:boolean;error?:string;warning?:string;helpText?:string;state?:KbxFieldState;emptyLabel?:string}>(),{state:'default',emptyLabel:'선택'})
const emit=defineEmits<{ 'update:modelValue':[string|null] }>()
const uid=`kbx-select-${Math.random().toString(36).slice(2)}`
const messageId=`${uid}-message`
const effectiveState=computed(()=>props.error?'error':props.warning?'warning':props.state)
const message=computed(()=>props.error||props.warning||props.helpText||'')
function update(event:Event){if(props.readonly)return;emit('update:modelValue',($eventTarget(event)).value||null)}
function $eventTarget(event:Event){return event.target as HTMLSelectElement}
</script>
<template><div class="kbx-field" :data-state="effectiveState"><label :for="uid">{{label}}<span v-if="required" aria-hidden="true"> *</span></label><select :id="uid" :value="modelValue??''" :disabled="disabled" :aria-disabled="disabled||undefined" :aria-readonly="readonly||undefined" :required="required" :aria-invalid="Boolean(error)" :aria-describedby="message?messageId:undefined" @change="update"><option value="">{{emptyLabel}}</option><option v-for="option in options" :key="option.value" :value="option.value" :disabled="option.disabled">{{option.label}}</option></select><span v-if="message" :id="messageId" class="kbx-field__message" :role="error?'alert':undefined">{{message}}</span></div></template>
<style scoped>.kbx-field{display:grid;grid-template-columns:var(--kbx-label-width) minmax(0,1fr);gap:var(--kbx-space-2);align-items:center;font-size:var(--kbx-font-md)}.kbx-field label{font-weight:500}.kbx-field select{height:var(--kbx-control-height);border:var(--kbx-border-width) solid var(--kbx-color-border-strong);border-radius:var(--kbx-radius-sm);padding:0 var(--kbx-space-2);background:var(--kbx-color-surface);font:inherit;color:var(--kbx-color-text)}.kbx-field[data-state="changed"] select{border-color:var(--kbx-color-changed-border);background:var(--kbx-color-changed-surface)}.kbx-field[data-state="warning"] select{border-color:var(--kbx-color-warning-border);background:var(--kbx-color-warning-surface)}.kbx-field[data-state="ai-suggested"] select{border-color:var(--kbx-color-ai-border);background:var(--kbx-color-ai-surface)}.kbx-field[data-state="error"] select{border-color:var(--kbx-color-danger);background:var(--kbx-color-danger-surface)}.kbx-field select[aria-readonly="true"]{pointer-events:none;background:var(--kbx-color-surface-muted)}.kbx-field select:disabled{background:var(--kbx-color-surface-subtle);color:var(--kbx-color-text-muted)}.kbx-field__message{grid-column:2;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.kbx-field[data-state="warning"] .kbx-field__message{color:var(--kbx-color-warning-text)}.kbx-field[data-state="error"] .kbx-field__message{color:var(--kbx-color-danger)}.kbx-field[data-state="ai-suggested"] .kbx-field__message{color:var(--kbx-color-ai-text)}</style>