Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/packages/kbx-ui/src/components/KbxNumberField.vue
T

48 lines
3.7 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import type { KbxFieldState } from '@kbx/contracts'
const props = withDefaults(defineProps<{
modelValue?: number | null
label: string
required?: boolean
readonly?: boolean
disabled?: boolean
error?: string
warning?: string
helpText?: string
state?: KbxFieldState
min?: number
max?: number
step?: number
precision?: number
suffix?: string
}>(), { step:1, precision:0, state:'default' })
const emit=defineEmits<{ 'update:modelValue':[number|null]; enter:[] }>()
const uid=`kbx-number-${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 parse(raw:string){
if(!raw.trim()){emit('update:modelValue',null);return}
const n=Number(raw.replaceAll(',',''))
if(!Number.isFinite(n))return
const bounded=Math.max(props.min??-Infinity,Math.min(props.max??Infinity,n))
emit('update:modelValue',bounded)
}
function display(v:number|null|undefined){if(v==null)return'';return new Intl.NumberFormat('ko-KR',{minimumFractionDigits:props.precision,maximumFractionDigits:props.precision}).format(v)}
</script>
<template>
<div class="kbx-field" :data-state="effectiveState">
<label :for="uid" class="kbx-field__label">{{label}}<span v-if="required" aria-hidden="true"> *</span></label>
<div class="kbx-number-wrap">
<input :id="uid" class="kbx-number" inputmode="decimal" :value="display(modelValue)" :readonly="readonly" :disabled="disabled" :required="required" :aria-invalid="Boolean(error)" :aria-describedby="message?messageId:undefined" @change="parse(($event.target as HTMLInputElement).value)" @keydown.enter.prevent="emit('enter')">
<span v-if="suffix" class="kbx-number__suffix">{{suffix}}</span>
</div>
<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:start;font-size:var(--kbx-font-md)}.kbx-field__label{padding-top:var(--kbx-space-2);font-weight:500}.kbx-number-wrap{display:flex;align-items:center;gap:var(--kbx-space-2)}.kbx-number{height:var(--kbx-control-height);width:100%;min-width:0;text-align:right;border:var(--kbx-border-width) solid var(--kbx-color-border-strong);border-radius:var(--kbx-radius-sm);padding:0 var(--kbx-space-3);font:inherit;background:var(--kbx-color-surface);color:var(--kbx-color-text)}.kbx-field[data-state="changed"] .kbx-number{border-color:var(--kbx-color-changed-border);background:var(--kbx-color-changed-surface)}.kbx-field[data-state="warning"] .kbx-number{border-color:var(--kbx-color-warning-border);background:var(--kbx-color-warning-surface)}.kbx-field[data-state="ai-suggested"] .kbx-number{border-color:var(--kbx-color-ai-border);background:var(--kbx-color-ai-surface)}.kbx-field[data-state="error"] .kbx-number{border-color:var(--kbx-color-danger);background:var(--kbx-color-danger-surface)}.kbx-number[readonly]{background:var(--kbx-color-surface-muted)}.kbx-number:disabled{background:var(--kbx-color-surface-subtle);color:var(--kbx-color-text-muted)}.kbx-number__suffix{white-space:nowrap;color:var(--kbx-color-text-muted)}.kbx-field__message{grid-column:2;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs);margin-top:calc(var(--kbx-space-1) * -1)}.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>