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>
115 lines
2.2 KiB
Vue
115 lines
2.2 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* KBX Number Field Component — v60
|
|
*/
|
|
|
|
withDefaults(
|
|
defineProps<{
|
|
modelValue: number | string
|
|
placeholder?: string
|
|
label?: string
|
|
error?: string
|
|
required?: boolean
|
|
readonly?: boolean
|
|
disabled?: boolean
|
|
min?: number
|
|
max?: number
|
|
step?: number
|
|
}>(),
|
|
{
|
|
modelValue: '',
|
|
step: 1,
|
|
}
|
|
)
|
|
|
|
defineEmits<{
|
|
'update:modelValue': [value: number]
|
|
blur: []
|
|
}>()
|
|
</script>
|
|
|
|
<template>
|
|
<div class="kbx-number-wrapper">
|
|
<label v-if="label" class="kbx-number__label">
|
|
{{ label }}
|
|
<span v-if="required" class="kbx-number__required">*</span>
|
|
</label>
|
|
<input
|
|
:value="modelValue"
|
|
type="number"
|
|
:placeholder="placeholder"
|
|
:readonly="readonly"
|
|
:disabled="disabled"
|
|
:min="min"
|
|
:max="max"
|
|
:step="step"
|
|
:class="['kbx-number', { 'is-error': error }]"
|
|
@input="$emit('update:modelValue', Number(($event.target as HTMLInputElement).value))"
|
|
@blur="$emit('blur')"
|
|
/>
|
|
<div v-if="error" class="kbx-number__error">{{ error }}</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.kbx-number-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.kbx-number__label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--kbx-color-text, #000);
|
|
}
|
|
|
|
.kbx-number__required {
|
|
color: var(--kbx-color-danger, #ef4444);
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.kbx-number {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--kbx-color-border, #e5e7eb);
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
background: var(--kbx-color-surface, #fff);
|
|
color: var(--kbx-color-text, #000);
|
|
text-align: right;
|
|
min-height: 34px;
|
|
}
|
|
|
|
.kbx-number:focus {
|
|
outline: none;
|
|
border-color: var(--kbx-color-primary, #3b82f6);
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.kbx-number:disabled {
|
|
background: var(--kbx-color-border, #e5e7eb);
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.kbx-number.is-error {
|
|
border-color: var(--kbx-color-danger, #ef4444);
|
|
}
|
|
|
|
.kbx-number__error {
|
|
font-size: 12px;
|
|
color: var(--kbx-color-danger, #ef4444);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.kbx-number__label {
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.kbx-number {
|
|
background: #1f2937;
|
|
border-color: #374151;
|
|
color: #f9fafb;
|
|
}
|
|
}
|
|
</style>
|