Input.vue 3.27 KB
<script setup>
/**
 * Input —— 受控单行输入框(v-model)。与业务无关,只认 props。
 *
 * @typedef {'sm'|'md'|'lg'} InputSize
 * @typedef {'text'|'password'|'number'|'email'|'search'|'url'|'date'|'datetime-local'} InputType
 */

defineProps({
  /** 受控值。 */
  modelValue: { type: [String, Number], default: '' },
  /** @type {import('vue').PropType<InputType>} 原生 input 类型。 */
  type: { type: String, default: 'text' },
  /** 占位文案。 */
  placeholder: { type: String, default: '' },
  /** 禁用态:阻断输入且降透明度。 */
  disabled: { type: Boolean, default: false },
  /** 非法态:描边转 danger 色,并置 aria-invalid。 */
  invalid: { type: Boolean, default: false },
  /** @type {import('vue').PropType<InputSize>} 尺寸。 */
  size: { type: String, default: 'md' },
})

const emit = defineEmits(['update:modelValue'])

/**
 * 转发输入值。
 * @param {Event} e
 * @returns {void}
 */
function onInput(e) {
  emit('update:modelValue', /** @type {HTMLInputElement} */ (e.target).value)
}
</script>

<template>
  <input
    class="drama-input"
    :class="[`drama-input--${size}`, { 'is-invalid': invalid }]"
    :type="type"
    :value="modelValue"
    :placeholder="placeholder"
    :disabled="disabled"
    :aria-invalid="invalid ? 'true' : 'false'"
    @input="onInput"
  >
</template>

<style scoped>
/*
 * 底色跟所在卡片一致(surface),只留一圈发丝描边 —— 控件不是比卡片更深的一块「凹陷」。
 * 用 surface-alt 填充 + border-strong 描边(填充和描边双重强调)时,一排筛选读起来像一格格禁用的死块,
 * 值反而是里面最不显眼的东西。层次交给交互态:hover 描边加深,focus 转主色 + 光晕。
 * Select / Textarea / SegmentedControl 同此,一排控件才是一套。
 */
.drama-input {
  box-sizing: border-box;
  width: 100%;
  border: 1px solid var(--drama-color-border);
  border-radius: var(--drama-radius-sm);
  background-color: var(--drama-color-surface);
  color: var(--drama-color-text);
  font-family: var(--drama-font-family-body);
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}

.drama-input::placeholder {
  color: var(--drama-color-text-muted);
}

.drama-input:hover:not(:disabled):not(:focus) {
  border-color: var(--drama-color-border-strong);
}

.drama-input:focus {
  border-color: var(--drama-color-primary);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--drama-color-primary) 18%, transparent);
}

.drama-input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.drama-input.is-invalid {
  border-color: var(--drama-color-danger);
}

.drama-input.is-invalid:focus {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--drama-color-danger) 18%, transparent);
}

/* 高度取 control token,不再由 padding 推 —— 同尺寸的 Select / SegmentedControl / Button 与它等高。 */
.drama-input--sm {
  height: var(--drama-control-height-sm);
  padding: 0 var(--drama-space-md);
  font-size: var(--drama-font-size-sm);
}

.drama-input--md {
  height: var(--drama-control-height-md);
  padding: 0 var(--drama-space-md);
  font-size: var(--drama-font-size-md);
}

.drama-input--lg {
  height: var(--drama-control-height-lg);
  padding: 0 var(--drama-space-lg);
  font-size: var(--drama-font-size-lg);
}
</style>